alchemy 2.0.0-beta.53 → 2.0.0-beta.55

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 (94) hide show
  1. package/bin/exec.js +1 -1
  2. package/bin/exec.js.map +1 -1
  3. package/lib/Cloudflare/AiGateway/AiGatewaySpendingLimit.d.ts +165 -0
  4. package/lib/Cloudflare/AiGateway/AiGatewaySpendingLimit.d.ts.map +1 -0
  5. package/lib/Cloudflare/AiGateway/AiGatewaySpendingLimit.js +232 -0
  6. package/lib/Cloudflare/AiGateway/AiGatewaySpendingLimit.js.map +1 -0
  7. package/lib/Cloudflare/AiGateway/index.d.ts +1 -0
  8. package/lib/Cloudflare/AiGateway/index.d.ts.map +1 -1
  9. package/lib/Cloudflare/AiGateway/index.js +1 -0
  10. package/lib/Cloudflare/AiGateway/index.js.map +1 -1
  11. package/lib/Cloudflare/Bridge.d.ts +1 -0
  12. package/lib/Cloudflare/Bridge.d.ts.map +1 -1
  13. package/lib/Cloudflare/Bridge.js +1 -0
  14. package/lib/Cloudflare/Bridge.js.map +1 -1
  15. package/lib/Cloudflare/Fetcher.d.ts +2 -2
  16. package/lib/Cloudflare/Fetcher.d.ts.map +1 -1
  17. package/lib/Cloudflare/Fetcher.js.map +1 -1
  18. package/lib/Cloudflare/Hyperdrive/Hyperdrive.d.ts.map +1 -1
  19. package/lib/Cloudflare/Hyperdrive/Hyperdrive.js +6 -6
  20. package/lib/Cloudflare/Hyperdrive/Hyperdrive.js.map +1 -1
  21. package/lib/Cloudflare/Local.js +5 -2
  22. package/lib/Cloudflare/Local.js.map +1 -1
  23. package/lib/Cloudflare/LocalRuntime.d.ts +21 -0
  24. package/lib/Cloudflare/LocalRuntime.d.ts.map +1 -0
  25. package/lib/Cloudflare/LocalRuntime.js +40 -0
  26. package/lib/Cloudflare/LocalRuntime.js.map +1 -0
  27. package/lib/Cloudflare/Providers.d.ts +1 -1
  28. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  29. package/lib/Cloudflare/Providers.js +3 -1
  30. package/lib/Cloudflare/Providers.js.map +1 -1
  31. package/lib/Cloudflare/Queue/Queue.d.ts +4 -1
  32. package/lib/Cloudflare/Queue/Queue.d.ts.map +1 -1
  33. package/lib/Cloudflare/Queue/Queue.js +61 -2
  34. package/lib/Cloudflare/Queue/Queue.js.map +1 -1
  35. package/lib/Cloudflare/Queue/QueueConsumer.d.ts +32 -26
  36. package/lib/Cloudflare/Queue/QueueConsumer.d.ts.map +1 -1
  37. package/lib/Cloudflare/Queue/QueueConsumer.js +73 -3
  38. package/lib/Cloudflare/Queue/QueueConsumer.js.map +1 -1
  39. package/lib/Cloudflare/Workers/Assets.js +2 -2
  40. package/lib/Cloudflare/Workers/Assets.js.map +1 -1
  41. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +3 -5
  42. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  43. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +32 -25
  44. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  45. package/lib/Cloudflare/Workers/Worker.d.ts +2 -4
  46. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  47. package/lib/Cloudflare/Workers/Worker.js +5 -7
  48. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  49. package/lib/Local/ProviderLayer.d.ts +10 -0
  50. package/lib/Local/ProviderLayer.d.ts.map +1 -0
  51. package/lib/Local/ProviderLayer.js +7 -0
  52. package/lib/Local/ProviderLayer.js.map +1 -0
  53. package/lib/Local/RpcProvider.d.ts +4 -3
  54. package/lib/Local/RpcProvider.d.ts.map +1 -1
  55. package/lib/Local/RpcProvider.js +6 -2
  56. package/lib/Local/RpcProvider.js.map +1 -1
  57. package/lib/Local/RpcProviderProxy.js.map +1 -1
  58. package/lib/Local/RpcSerialization.js +2 -2
  59. package/lib/Local/RpcSerialization.js.map +1 -1
  60. package/lib/Planetscale/Branch.d.ts.map +1 -1
  61. package/lib/Planetscale/Branch.js +25 -3
  62. package/lib/Planetscale/Branch.js.map +1 -1
  63. package/lib/Planetscale/Database.d.ts +1 -1
  64. package/lib/Planetscale/Postgres/PostgresClusterSize.d.ts +1 -1
  65. package/lib/Planetscale/Postgres/PostgresClusterSize.d.ts.map +1 -1
  66. package/lib/Planetscale/Postgres/PostgresClusterSize.js +13 -3
  67. package/lib/Planetscale/Postgres/PostgresClusterSize.js.map +1 -1
  68. package/lib/Planetscale/Util.d.ts +1 -1
  69. package/lib/Planetscale/Util.d.ts.map +1 -1
  70. package/lib/Planetscale/Util.js +7 -5
  71. package/lib/Planetscale/Util.js.map +1 -1
  72. package/lib/tsconfig.test.tsbuildinfo +1 -1
  73. package/package.json +12 -12
  74. package/src/Cloudflare/AiGateway/AiGatewaySpendingLimit.ts +368 -0
  75. package/src/Cloudflare/AiGateway/index.ts +1 -0
  76. package/src/Cloudflare/Bridge.ts +1 -0
  77. package/src/Cloudflare/Fetcher.ts +8 -4
  78. package/src/Cloudflare/Hyperdrive/Hyperdrive.ts +6 -8
  79. package/src/Cloudflare/Local.ts +9 -5
  80. package/src/Cloudflare/LocalRuntime.ts +70 -0
  81. package/src/Cloudflare/Providers.ts +4 -0
  82. package/src/Cloudflare/Queue/Queue.ts +73 -2
  83. package/src/Cloudflare/Queue/QueueConsumer.ts +130 -28
  84. package/src/Cloudflare/Workers/Assets.ts +2 -2
  85. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +39 -30
  86. package/src/Cloudflare/Workers/Worker.ts +5 -26
  87. package/src/Local/ProviderLayer.ts +22 -0
  88. package/src/Local/RpcProvider.ts +11 -5
  89. package/src/Local/RpcProviderProxy.ts +2 -2
  90. package/src/Local/RpcSerialization.ts +2 -2
  91. package/src/Planetscale/Branch.ts +38 -10
  92. package/src/Planetscale/Database.ts +1 -1
  93. package/src/Planetscale/Postgres/PostgresClusterSize.ts +16 -2
  94. package/src/Planetscale/Util.ts +7 -5
@@ -1,12 +1,21 @@
1
1
  import * as queues from "@distilled.cloud/cloudflare/queues";
2
2
  import * as Effect from "effect/Effect";
3
+ import * as MutableHashMap from "effect/MutableHashMap";
3
4
  import * as Option from "effect/Option";
4
5
  import * as Stream from "effect/Stream";
5
6
  import { isResolved } from "../../Diff.ts";
7
+ import * as ProviderLayer from "../../Local/ProviderLayer.ts";
8
+ import * as RpcProvider from "../../Local/RpcProvider.ts";
6
9
  import { createPhysicalName } from "../../PhysicalName.ts";
7
10
  import * as Provider from "../../Provider.ts";
8
11
  import { Resource } from "../../Resource.ts";
9
12
  import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
13
+ import {
14
+ generateLocalId,
15
+ isLiveId,
16
+ LOCAL_ENTRY_URL,
17
+ LocalRuntimeState,
18
+ } from "../LocalRuntime.ts";
10
19
  import type { Providers } from "../Providers.ts";
11
20
  import { QueueBinding } from "./QueueBinding.ts";
12
21
 
@@ -96,12 +105,18 @@ export const Queue = Resource<Queue>("Cloudflare.Queue")({
96
105
  bind: QueueBinding.bind,
97
106
  });
98
107
 
99
- export const QueueProvider = () =>
108
+ export const QueueProviderLive = () =>
100
109
  Provider.succeed(Queue, {
101
- stables: ["queueId", "accountId"],
110
+ // The `queueId` is not marked as stable because if you start in dev mode, the ID will change on first deploy.
111
+ stables: ["accountId"],
102
112
  diff: Effect.fn(function* ({ id, olds = {}, news = {}, output }) {
103
113
  const { accountId } = yield* yield* CloudflareEnvironment;
104
114
  if (!isResolved(news)) return undefined;
115
+ // If the queueId is a `dev:` ID, we need to update to a live one.
116
+ // The live resource doesn't exist yet, so there's no need to replace even if the name or accountId changed.
117
+ if (!isLiveId(output?.queueId)) {
118
+ return { action: "update" };
119
+ }
105
120
  if ((output?.accountId ?? accountId) !== accountId) {
106
121
  return { action: "replace" } as const;
107
122
  }
@@ -171,6 +186,8 @@ export const QueueProvider = () =>
171
186
  };
172
187
  }),
173
188
  delete: Effect.fn(function* ({ output }) {
189
+ // If the queueId is a `dev:` ID, the resource only exists locally, so we don't need to delete it from Cloudflare.
190
+ if (!isLiveId(output.queueId)) return;
174
191
  yield* queues
175
192
  .deleteQueue({
176
193
  accountId: output.accountId,
@@ -228,3 +245,57 @@ const findQueueByName = Effect.fnUntraced(function* (queueName: string) {
228
245
  Effect.map(Option.getOrUndefined),
229
246
  );
230
247
  });
248
+
249
+ export const QueueProviderLocal = () =>
250
+ RpcProvider.effect(
251
+ Queue,
252
+ LOCAL_ENTRY_URL,
253
+ Effect.gen(function* () {
254
+ const localRuntimeState = yield* LocalRuntimeState;
255
+ return {
256
+ stables: ["accountId"],
257
+ diff: Effect.fn(function* ({ id, olds = {}, news = {}, output }) {
258
+ const { accountId } = yield* yield* CloudflareEnvironment;
259
+ if (!output?.queueId) return { action: "update" };
260
+ if (!isResolved(news)) return undefined;
261
+ const name = yield* createQueueName(id, news.name);
262
+ const oldName = output?.queueName
263
+ ? yield* createQueueName(id, olds.name)
264
+ : yield* createQueueName(id, olds.name);
265
+ if (name !== oldName || output.accountId !== accountId) {
266
+ return { action: "replace" };
267
+ }
268
+ // If the resource is a noop, add it to the local runtime state so it's available downstream.
269
+ // We do it here instead of in the reconcile function so it doesn't appear as an update.
270
+ MutableHashMap.set(localRuntimeState.queues, output.queueId, output);
271
+ return { action: "noop" };
272
+ }),
273
+ read: Effect.fn(function* ({ output }) {
274
+ if (!output?.queueId) return undefined;
275
+ return MutableHashMap.get(
276
+ localRuntimeState.queues,
277
+ output.queueId,
278
+ ).pipe(Option.getOrUndefined);
279
+ }),
280
+ reconcile: Effect.fn(function* ({ id, news = {}, output }) {
281
+ const { accountId } = yield* yield* CloudflareEnvironment;
282
+ const queue: Queue["Attributes"] = {
283
+ queueId: output?.queueId ?? generateLocalId(),
284
+ queueName: yield* createQueueName(id, news.name),
285
+ accountId: output?.accountId ?? accountId,
286
+ };
287
+ MutableHashMap.set(localRuntimeState.queues, queue.queueId, queue);
288
+ return queue;
289
+ }),
290
+ delete: Effect.fn(function* ({ output }) {
291
+ MutableHashMap.remove(localRuntimeState.queues, output.queueId);
292
+ }),
293
+ };
294
+ }),
295
+ );
296
+
297
+ export const QueueProvider = () =>
298
+ ProviderLayer.select({
299
+ local: () => QueueProviderLocal(),
300
+ live: () => QueueProviderLive(),
301
+ });
@@ -1,12 +1,20 @@
1
1
  import * as queues from "@distilled.cloud/cloudflare/queues";
2
2
  import * as Effect from "effect/Effect";
3
+ import * as MutableHashMap from "effect/MutableHashMap";
3
4
  import * as Option from "effect/Option";
4
5
  import * as Schedule from "effect/Schedule";
5
6
  import * as Stream from "effect/Stream";
6
7
  import { isResolved } from "../../Diff.ts";
8
+ import * as ProviderLayer from "../../Local/ProviderLayer.ts";
9
+ import * as RpcProvider from "../../Local/RpcProvider.ts";
7
10
  import * as Provider from "../../Provider.ts";
8
11
  import { Resource } from "../../Resource.ts";
9
12
  import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
13
+ import {
14
+ isLiveId,
15
+ LOCAL_ENTRY_URL,
16
+ LocalRuntimeState,
17
+ } from "../LocalRuntime.ts";
10
18
  import type { Providers } from "../Providers.ts";
11
19
 
12
20
  export type QueueConsumerProps = {
@@ -25,33 +33,35 @@ export type QueueConsumerProps = {
25
33
  /**
26
34
  * Consumer settings.
27
35
  */
28
- settings?: {
29
- /**
30
- * The maximum number of messages per batch.
31
- * @default 10
32
- */
33
- batchSize?: number;
34
- /**
35
- * The maximum number of concurrent consumer invocations.
36
- */
37
- maxConcurrency?: number;
38
- /**
39
- * The maximum number of retries for a message.
40
- * @default 3
41
- */
42
- maxRetries?: number;
43
- /**
44
- * The maximum time to wait for a batch to fill, in milliseconds.
45
- * @default 5000
46
- */
47
- maxWaitTimeMs?: number;
48
- /**
49
- * The number of seconds to wait before retrying a message.
50
- */
51
- retryDelay?: number;
52
- };
36
+ settings?: QueueConsumerSettings;
53
37
  };
54
38
 
39
+ export interface QueueConsumerSettings {
40
+ /**
41
+ * The maximum number of messages per batch.
42
+ * @default 10
43
+ */
44
+ batchSize?: number;
45
+ /**
46
+ * The maximum number of concurrent consumer invocations.
47
+ */
48
+ maxConcurrency?: number;
49
+ /**
50
+ * The maximum number of retries for a message.
51
+ * @default 3
52
+ */
53
+ maxRetries?: number;
54
+ /**
55
+ * The maximum time to wait for a batch to fill, in milliseconds.
56
+ * @default 5000
57
+ */
58
+ maxWaitTimeMs?: number;
59
+ /**
60
+ * The number of seconds to wait before retrying a message.
61
+ */
62
+ retryDelay?: number;
63
+ }
64
+
55
65
  export type QueueConsumer = Resource<
56
66
  "Cloudflare.QueueConsumer",
57
67
  QueueConsumerProps,
@@ -60,6 +70,8 @@ export type QueueConsumer = Resource<
60
70
  queueId: string;
61
71
  scriptName: string;
62
72
  accountId: string;
73
+ deadLetterQueue?: string;
74
+ settings?: QueueConsumerSettings;
63
75
  },
64
76
  never,
65
77
  Providers
@@ -116,12 +128,19 @@ const findWorkerConsumer = (acct: string, queueId: string) =>
116
128
  Stream.runHead,
117
129
  Effect.map(Option.getOrUndefined),
118
130
  );
119
- export const QueueConsumerProvider = () =>
131
+
132
+ export const QueueConsumerProviderLive = () =>
120
133
  Provider.succeed(QueueConsumer, {
121
- stables: ["consumerId", "accountId"],
134
+ // The `consumerId` is not marked as stable because if you start in dev mode, the ID will change on first deploy.
135
+ stables: ["accountId"],
122
136
  diff: Effect.fn(function* ({ olds, news, output }) {
123
137
  const { accountId } = yield* yield* CloudflareEnvironment;
124
138
  if (!isResolved(news)) return undefined;
139
+ // If either contain `dev:` IDs, we need to update to live ones.
140
+ // The live resource doesn't exist yet, so there's no need to replace even when we otherwise would.
141
+ if (!isLiveId(output?.queueId) || !isLiveId(output?.consumerId)) {
142
+ return { action: "update" };
143
+ }
125
144
  if ((output?.accountId ?? accountId) !== accountId) {
126
145
  return { action: "replace" } as const;
127
146
  }
@@ -167,7 +186,7 @@ export const QueueConsumerProvider = () =>
167
186
  // updating it could clobber another team's wiring.
168
187
  let observed: ObservedConsumer | undefined;
169
188
  let owned = false;
170
- if (output?.consumerId) {
189
+ if (isLiveId(output?.consumerId)) {
171
190
  const fetched = yield* queues
172
191
  .getConsumer({
173
192
  accountId: acct,
@@ -365,9 +384,14 @@ export const QueueConsumerProvider = () =>
365
384
  queueId,
366
385
  scriptName: news.scriptName!,
367
386
  accountId: acct,
387
+ deadLetterQueue: news.deadLetterQueue,
388
+ settings: news.settings,
368
389
  };
369
390
  }),
370
391
  delete: Effect.fn(function* ({ output }) {
392
+ // If the consumerId is a `dev:` ID, the resource only exists locally, so we don't need to delete it from Cloudflare.
393
+ if (!isLiveId(output.consumerId)) return;
394
+
371
395
  yield* queues
372
396
  .deleteConsumer({
373
397
  accountId: output.accountId,
@@ -421,6 +445,8 @@ export const QueueConsumerProvider = () =>
421
445
  ? fetched.scriptName
422
446
  : output.scriptName) ?? output.scriptName,
423
447
  accountId: output.accountId,
448
+ deadLetterQueue: output.deadLetterQueue,
449
+ settings: output.settings,
424
450
  };
425
451
  }
426
452
  }
@@ -439,6 +465,8 @@ export const QueueConsumerProvider = () =>
439
465
  queueId: output.queueId,
440
466
  scriptName: match.script ?? output.scriptName,
441
467
  accountId: output.accountId,
468
+ deadLetterQueue: output.deadLetterQueue,
469
+ settings: output.settings,
442
470
  };
443
471
  }
444
472
  }
@@ -465,3 +493,77 @@ const toObserved = (c: {
465
493
  const queueHandlerReadinessSchedule = Schedule.spaced("2 seconds").pipe(
466
494
  Schedule.both(Schedule.recurs(30)),
467
495
  );
496
+
497
+ export const QueueConsumerProviderLocal = () =>
498
+ RpcProvider.effect(
499
+ QueueConsumer,
500
+ LOCAL_ENTRY_URL,
501
+ Effect.gen(function* () {
502
+ const localRuntimeState = yield* LocalRuntimeState;
503
+ return {
504
+ diff: Effect.fn(function* ({ news, output }) {
505
+ const { accountId } = yield* yield* CloudflareEnvironment;
506
+ if (!output) return { action: "update" };
507
+ if (!isResolved(news)) return undefined;
508
+ if (
509
+ output.queueId !== news.queueId ||
510
+ output.accountId !== accountId
511
+ ) {
512
+ return { action: "replace" };
513
+ }
514
+ if (
515
+ JSON.stringify(output.settings ?? {}) !==
516
+ JSON.stringify(news.settings ?? {}) ||
517
+ output.scriptName !== news.scriptName ||
518
+ output.deadLetterQueue !== news.deadLetterQueue
519
+ ) {
520
+ return { action: "update" };
521
+ }
522
+ // If the resource is a noop, add it to the local runtime state so it's available downstream.
523
+ // We do it here instead of in the reconcile function so it doesn't appear as an update.
524
+ MutableHashMap.set(
525
+ localRuntimeState.queueConsumers,
526
+ output.consumerId,
527
+ output,
528
+ );
529
+ return { action: "noop" };
530
+ }),
531
+ read: Effect.fn(function* ({ output }) {
532
+ if (!output?.consumerId) return undefined;
533
+ return MutableHashMap.get(
534
+ localRuntimeState.queueConsumers,
535
+ output.consumerId,
536
+ ).pipe(Option.getOrUndefined);
537
+ }),
538
+ reconcile: Effect.fn(function* ({ news, output }) {
539
+ const { accountId } = yield* yield* CloudflareEnvironment;
540
+ const consumer: QueueConsumer["Attributes"] = {
541
+ consumerId: output?.consumerId ?? `dev:${crypto.randomUUID()}`,
542
+ queueId: news.queueId,
543
+ scriptName: news.scriptName,
544
+ deadLetterQueue: news.deadLetterQueue,
545
+ accountId,
546
+ settings: news.settings,
547
+ };
548
+ MutableHashMap.set(
549
+ localRuntimeState.queueConsumers,
550
+ consumer.consumerId,
551
+ consumer,
552
+ );
553
+ return consumer;
554
+ }),
555
+ delete: Effect.fn(function* ({ output }) {
556
+ MutableHashMap.remove(
557
+ localRuntimeState.queueConsumers,
558
+ output.consumerId,
559
+ );
560
+ }),
561
+ };
562
+ }),
563
+ );
564
+
565
+ export const QueueConsumerProvider = () =>
566
+ ProviderLayer.select({
567
+ local: () => QueueConsumerProviderLocal(),
568
+ live: () => QueueConsumerProviderLive(),
569
+ });
@@ -1,4 +1,4 @@
1
- import { Ignore } from "@alchemy.run/node-utils/ignore";
1
+ import createIgnore from "@alchemy.run/node-utils/ignore";
2
2
  import * as workers from "@distilled.cloud/cloudflare/workers";
3
3
  import * as Data from "effect/Data";
4
4
  import * as Effect from "effect/Effect";
@@ -98,7 +98,7 @@ const maybeReadString = Effect.fnUntraced(function* (file: string) {
98
98
  });
99
99
 
100
100
  const createIgnoreMatcher = (patterns: string[]) => {
101
- const matcher = new Ignore().add(patterns);
101
+ const matcher = createIgnore().add(patterns);
102
102
  return (file: string) => matcher.ignores(file);
103
103
  };
104
104
 
@@ -1,5 +1,4 @@
1
1
  import {
2
- layerRuntime,
3
2
  Runtime,
4
3
  RuntimeError,
5
4
  type BindingHook,
@@ -8,6 +7,7 @@ import {
8
7
  type Module,
9
8
  type Assets as RuntimeAssets,
10
9
  type DurableObjectNamespace as RuntimeDurableObjectNamespace,
10
+ type QueueConsumer as RuntimeQueueConsumer,
11
11
  type RuntimeServices,
12
12
  } from "@distilled.cloud/cloudflare-runtime";
13
13
  import {
@@ -26,6 +26,7 @@ import {
26
26
  KvNamespace,
27
27
  MtlsCertificate,
28
28
  Pipelines,
29
+ Queue,
29
30
  R2Bucket,
30
31
  RateLimit,
31
32
  SendEmail,
@@ -44,19 +45,21 @@ import * as Equal from "effect/Equal";
44
45
  import * as Exit from "effect/Exit";
45
46
  import * as Fiber from "effect/Fiber";
46
47
  import * as Hash from "effect/Hash";
48
+ import * as MutableHashMap from "effect/MutableHashMap";
49
+ import * as Option from "effect/Option";
47
50
  import * as Path from "effect/Path";
48
51
  import * as Redacted from "effect/Redacted";
49
52
  import * as Result from "effect/Result";
50
53
  import * as Schema from "effect/Schema";
51
54
  import * as Scope from "effect/Scope";
52
55
  import * as Stream from "effect/Stream";
53
- import { AlchemyContext } from "../../AlchemyContext.ts";
54
56
  import type * as Bundle from "../../Bundle/Bundle.ts";
55
57
  import { isResolved } from "../../Diff.ts";
56
58
  import * as RpcProvider from "../../Local/RpcProvider.ts";
57
59
  import type { ResourceBinding } from "../../Resource.ts";
58
60
  import { Stack } from "../../Stack.ts";
59
61
  import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
62
+ import { LOCAL_ENTRY_URL, LocalRuntimeState } from "../LocalRuntime.ts";
60
63
  import type { WorkerAssetsConfig, WorkerProps } from "../Workers/Worker.ts";
61
64
  import { getCompatibility } from "./Compatibility.ts";
62
65
  import * as Vite from "./Vite.ts";
@@ -79,41 +82,16 @@ export class WorkerValidationError extends Schema.TaggedErrorClass<WorkerValidat
79
82
  },
80
83
  ) {}
81
84
 
82
- export const localRuntimeServices = () =>
83
- RpcProvider.providerServicesEffect(
84
- Effect.gen(function* () {
85
- const { accountId } = yield* yield* CloudflareEnvironment;
86
- const { dotAlchemy } = yield* AlchemyContext;
87
- const path = yield* Path.Path;
88
- return layerRuntime({
89
- api: {
90
- accountId,
91
- },
92
- storage: {
93
- directory: path.join(dotAlchemy, "local"),
94
- },
95
- });
96
- }),
97
- );
98
-
99
85
  export const LocalWorkerProvider = () =>
100
86
  RpcProvider.effect(
101
87
  Worker,
102
- import.meta.resolve(
103
- // `import.meta.resolve(<string>)` is a runtime API — TypeScript's
104
- // `rewriteRelativeImportExtensions` does NOT touch the string literal, so
105
- // we have to pick the right extension ourselves. `import.meta.url` reflects
106
- // the actual on-disk extension of *this* file (`.ts` when loaded from
107
- // `src/` under Bun or vitest, `.js` when loaded from the compiled `lib/`
108
- // under Node), which is exactly the signal we need.
109
- import.meta.url.endsWith(".ts") ? "../Local.ts" : "../Local.js",
110
- import.meta.url,
111
- ),
88
+ LOCAL_ENTRY_URL,
112
89
  Effect.gen(function* () {
113
90
  const bundler = yield* WorkerBundle;
114
91
  const runtime = yield* Runtime;
115
92
  const stack = yield* Stack;
116
93
  const path = yield* Path.Path;
94
+ const localRuntimeState = yield* LocalRuntimeState;
117
95
  const workerProxy = yield* WorkerProxy.WorkerProxy;
118
96
  const proxyInstances = new Map<
119
97
  string,
@@ -124,6 +102,32 @@ export const LocalWorkerProvider = () =>
124
102
  }
125
103
  >();
126
104
 
105
+ const getQueueConsumers = Effect.fnUntraced(function* (
106
+ scriptName: string,
107
+ ) {
108
+ const consumers: RuntimeQueueConsumer[] = [];
109
+ for (const consumer of MutableHashMap.values(
110
+ localRuntimeState.queueConsumers,
111
+ )) {
112
+ if (consumer.scriptName === scriptName) {
113
+ const queue = MutableHashMap.get(
114
+ localRuntimeState.queues,
115
+ consumer.queueId,
116
+ ).pipe(Option.getOrUndefined);
117
+ if (queue) {
118
+ consumers.push({
119
+ queueName: queue.queueName,
120
+ deadLetterQueue: consumer.deadLetterQueue,
121
+ ...consumer.settings,
122
+ });
123
+ } else {
124
+ return yield* Effect.die(`Queue ${consumer.queueId} not found`);
125
+ }
126
+ }
127
+ }
128
+ return consumers;
129
+ });
130
+
127
131
  const startProxy = Effect.fn(function* (
128
132
  id: string,
129
133
  serverOptions: WorkerConfig["dev"],
@@ -211,6 +215,7 @@ export const LocalWorkerProvider = () =>
211
215
  durableObjectNamespaces: toRuntimeDurableObjectNamespaces(
212
216
  worker.durableObjectNamespaces,
213
217
  ),
218
+ queueConsumers: yield* getQueueConsumers(worker.name),
214
219
  modules: yield* toRuntimeModules(bundle),
215
220
  assets: toRuntimeAssets(worker.assets),
216
221
  })
@@ -392,6 +397,7 @@ export const LocalWorkerProvider = () =>
392
397
  worker.durableObjectNamespaces,
393
398
  ),
394
399
  hyperdrives: worker.hyperdrives,
400
+ queueConsumers: yield* getQueueConsumers(worker.name),
395
401
  assets: toRuntimeAssets(worker.assets),
396
402
  },
397
403
  context,
@@ -586,7 +592,10 @@ const toRuntimeBinding = Effect.fnUntraced(function* (b: WorkerBinding) {
586
592
  case "plain_text":
587
593
  return Text.local(b.name, b.text);
588
594
  case "queue":
589
- return yield* unsupported();
595
+ return Queue.local({
596
+ binding: b.name,
597
+ queueName: b.queueName,
598
+ });
590
599
  case "r2_bucket":
591
600
  return R2Bucket.remote(b.name, b.bucketName, b.jurisdiction);
592
601
  case "ratelimit":
@@ -6,20 +6,19 @@ import type { ConfigError } from "effect/Config";
6
6
  import * as Context from "effect/Context";
7
7
  import * as Data from "effect/Data";
8
8
  import * as Effect from "effect/Effect";
9
- import * as Layer from "effect/Layer";
10
9
  import * as Path from "effect/Path";
11
10
  import * as Predicate from "effect/Predicate";
12
11
  import * as Redacted from "effect/Redacted";
13
12
  import * as Schedule from "effect/Schedule";
14
13
  import * as crypto from "node:crypto";
15
14
  import { Unowned } from "../../AdoptPolicy.ts";
16
- import { AlchemyContext } from "../../AlchemyContext.ts";
17
15
  import * as Artifacts from "../../Artifacts.ts";
18
16
  import { hashDirectory, type MemoOptions } from "../../Build/Memo.ts";
19
17
  import * as Bundle from "../../Bundle/Bundle.ts";
20
18
  import type { ScopedPlanStatusSession } from "../../Cli/Cli.ts";
21
19
  import { isResolved } from "../../Diff.ts";
22
20
  import type { InputProps } from "../../Input.ts";
21
+ import * as ProviderLayer from "../../Local/ProviderLayer.ts";
23
22
  import { Platform, type Main, type PlatformProps } from "../../Platform.ts";
24
23
  import * as Provider from "../../Provider.ts";
25
24
  import { Resource, type ResourceBinding } from "../../Resource.ts";
@@ -36,10 +35,7 @@ import {
36
35
  } from "./Assets.ts";
37
36
  import { getCompatibility } from "./Compatibility.ts";
38
37
  import { isDurableObjectExport } from "./DurableObjectNamespace.ts";
39
- import {
40
- LocalWorkerProvider,
41
- localRuntimeServices,
42
- } from "./LocalWorkerProvider.ts";
38
+ import { LocalWorkerProvider } from "./LocalWorkerProvider.ts";
43
39
  import { Request } from "./Request.ts";
44
40
  import * as Vite from "./Vite.ts";
45
41
  import {
@@ -743,27 +739,10 @@ class MissingDurableObjectNamespaces extends Data.TaggedError(
743
739
  expected: string[];
744
740
  }> {}
745
741
 
746
- const selectLayer = <
747
- LayerLive extends Layer.Layer<any, any, any>,
748
- LayerDev extends Layer.Layer<any, any, any>,
749
- >(input: {
750
- live: () => LayerLive;
751
- dev: () => LayerDev;
752
- }): Layer.Layer<
753
- Layer.Success<LayerLive | LayerDev>,
754
- Layer.Error<LayerLive | LayerDev>,
755
- Layer.Services<LayerLive | LayerDev> | AlchemyContext
756
- > =>
757
- Layer.unwrap(
758
- AlchemyContext.useSync((context) =>
759
- context.dev ? input.dev() : input.live(),
760
- ),
761
- );
762
-
763
742
  export const WorkerProvider = () =>
764
- selectLayer({
765
- live: LiveWorkerProvider,
766
- dev: () => Layer.provide(LocalWorkerProvider(), localRuntimeServices()),
743
+ ProviderLayer.select({
744
+ live: () => LiveWorkerProvider(),
745
+ local: () => LocalWorkerProvider(),
767
746
  });
768
747
 
769
748
  export const LiveWorkerProvider = () =>
@@ -0,0 +1,22 @@
1
+ import * as Layer from "effect/Layer";
2
+ import { AlchemyContext } from "../AlchemyContext.ts";
3
+
4
+ /**
5
+ * Returns a layer that selects the live or local provider based on the `AlchemyContext.dev` flag.
6
+ */
7
+ export const select = <
8
+ LayerLive extends Layer.Layer<any, any, any>,
9
+ LayerLocal extends Layer.Layer<any, any, any>,
10
+ >(input: {
11
+ live: () => LayerLive;
12
+ local: () => LayerLocal;
13
+ }): Layer.Layer<
14
+ Layer.Success<LayerLive | LayerLocal>,
15
+ Layer.Error<LayerLive | LayerLocal>,
16
+ Layer.Services<LayerLive | LayerLocal> | AlchemyContext
17
+ > =>
18
+ Layer.unwrap(
19
+ AlchemyContext.useSync((context) =>
20
+ context.dev ? input.local() : input.live(),
21
+ ),
22
+ );
@@ -5,6 +5,7 @@ import * as Option from "effect/Option";
5
5
  import * as Predicate from "effect/Predicate";
6
6
  import type { Scope } from "effect/Scope";
7
7
  import * as Stream from "effect/Stream";
8
+ import { AlchemyContext } from "../AlchemyContext.ts";
8
9
  import { InstanceId } from "../InstanceId.ts";
9
10
  import type { Platform } from "../Platform.ts";
10
11
  import * as Provider from "../Provider.ts";
@@ -76,6 +77,7 @@ export const effect = <
76
77
  cls,
77
78
  Effect.gen(function* () {
78
79
  const client = yield* Effect.serviceOption(RpcProviderProxy);
80
+ const context = yield* Effect.context();
79
81
  const stack = yield* Stack;
80
82
 
81
83
  if (client._tag === "None") {
@@ -87,6 +89,7 @@ export const effect = <
87
89
  return (...args: any[]) => {
88
90
  const result = value(...args);
89
91
  const services = Layer.mergeAll(
92
+ Layer.succeedContext(context),
90
93
  layerFallback(Stack, stack),
91
94
  layerFallback(Stage, stack.stage),
92
95
  Predicate.hasProperty(args[0], "instanceId") &&
@@ -127,7 +130,8 @@ const layerFallback = <I, S>(
127
130
  */
128
131
  export const providerServices = <ROut, E, RIn>(
129
132
  self: Layer.Layer<ROut, E, RIn>,
130
- ): Layer.Layer<ROut, E, RIn> => providerServicesEffect(Effect.succeed(self));
133
+ ): Layer.Layer<ROut, E, RIn | AlchemyContext> =>
134
+ providerServicesEffect(Effect.succeed(self));
131
135
 
132
136
  /**
133
137
  * Conditionally constructs a layer for use by an RpcProvider.
@@ -137,10 +141,12 @@ export const providerServices = <ROut, E, RIn>(
137
141
  */
138
142
  export const providerServicesEffect = <A, E1, R1, E, R>(
139
143
  self: Effect.Effect<Layer.Layer<A, E1, R1>, E, R>,
140
- ): Layer.Layer<A, E | E1, R1 | Exclude<R, Scope>> =>
141
- Effect.serviceOption(RpcProviderProxy).pipe(
142
- Effect.flatMap((client) =>
143
- client._tag === "None" ? self : (Effect.succeed(Layer.empty) as never),
144
+ ): Layer.Layer<A, E | E1, R1 | Exclude<R, Scope> | AlchemyContext> =>
145
+ Effect.zip(AlchemyContext, Effect.serviceOption(RpcProviderProxy)).pipe(
146
+ Effect.flatMap(([context, client]) =>
147
+ context.dev && client._tag === "None"
148
+ ? self
149
+ : (Effect.succeed(Layer.empty) as never),
144
150
  ),
145
151
  Layer.unwrap,
146
152
  );
@@ -1,4 +1,4 @@
1
- import { newWebSocketRpcSession, type RpcStub } from "capnweb";
1
+ import { newWebSocketRpcSession } from "capnweb";
2
2
  import * as Cache from "effect/Cache";
3
3
  import * as Config from "effect/Config";
4
4
  import * as Context from "effect/Context";
@@ -42,7 +42,7 @@ const make = Effect.fnUntraced(function* (spawnerUrl: string) {
42
42
  body: yield* HttpBody.json(payload),
43
43
  });
44
44
  const websocketUrl = yield* response.text;
45
- return newWebSocketRpcSession(websocketUrl) as RpcStub<RpcProxyApi>;
45
+ return newWebSocketRpcSession<RpcProxyApi>(websocketUrl);
46
46
  },
47
47
  (effect, serverEntryUrl) =>
48
48
  Effect.catch(effect, (error) =>
@@ -64,7 +64,7 @@ export const wrapRpcHandlers = <T extends Record<string, any>>(
64
64
  ? streamKeys?.includes(key)
65
65
  ? wrapRpcStreamHandler(value)
66
66
  : wrapRpcEffectHandler(value)
67
- : typeof value === "object" && value !== null
67
+ : typeof value === "object" && value !== null && !Array.isArray(value)
68
68
  ? wrapRpcHandlers(value)
69
69
  : value,
70
70
  ]),
@@ -82,7 +82,7 @@ export const unwrapRpcHandlers = <T extends Record<string, any>>(
82
82
  ? streamKeys?.includes(key)
83
83
  ? unwrapRpcStreamHandler(value)
84
84
  : unwrapRpcEffectHandler(value)
85
- : typeof value === "object" && value !== null
85
+ : typeof value === "object" && value !== null && !Array.isArray(value)
86
86
  ? unwrapRpcHandlers(value)
87
87
  : value,
88
88
  ]),