alchemy 2.0.0-beta.53 → 2.0.0-beta.54
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.
- package/bin/exec.js +1 -1
- package/bin/exec.js.map +1 -1
- package/lib/Cloudflare/Hyperdrive/Hyperdrive.d.ts.map +1 -1
- package/lib/Cloudflare/Hyperdrive/Hyperdrive.js +6 -6
- package/lib/Cloudflare/Hyperdrive/Hyperdrive.js.map +1 -1
- package/lib/Cloudflare/Local.js +5 -2
- package/lib/Cloudflare/Local.js.map +1 -1
- package/lib/Cloudflare/LocalRuntime.d.ts +21 -0
- package/lib/Cloudflare/LocalRuntime.d.ts.map +1 -0
- package/lib/Cloudflare/LocalRuntime.js +40 -0
- package/lib/Cloudflare/LocalRuntime.js.map +1 -0
- package/lib/Cloudflare/Providers.d.ts +1 -1
- package/lib/Cloudflare/Providers.d.ts.map +1 -1
- package/lib/Cloudflare/Providers.js +2 -1
- package/lib/Cloudflare/Providers.js.map +1 -1
- package/lib/Cloudflare/Queue/Queue.d.ts +4 -1
- package/lib/Cloudflare/Queue/Queue.d.ts.map +1 -1
- package/lib/Cloudflare/Queue/Queue.js +61 -2
- package/lib/Cloudflare/Queue/Queue.js.map +1 -1
- package/lib/Cloudflare/Queue/QueueConsumer.d.ts +32 -26
- package/lib/Cloudflare/Queue/QueueConsumer.d.ts.map +1 -1
- package/lib/Cloudflare/Queue/QueueConsumer.js +73 -3
- package/lib/Cloudflare/Queue/QueueConsumer.js.map +1 -1
- package/lib/Cloudflare/Workers/Assets.js +2 -2
- package/lib/Cloudflare/Workers/Assets.js.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +3 -5
- package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js +32 -25
- package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +2 -4
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +5 -7
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Local/ProviderLayer.d.ts +10 -0
- package/lib/Local/ProviderLayer.d.ts.map +1 -0
- package/lib/Local/ProviderLayer.js +7 -0
- package/lib/Local/ProviderLayer.js.map +1 -0
- package/lib/Local/RpcProvider.d.ts.map +1 -1
- package/lib/Local/RpcProvider.js +2 -1
- package/lib/Local/RpcProvider.js.map +1 -1
- package/lib/Local/RpcProviderProxy.js.map +1 -1
- package/lib/Local/RpcSerialization.js +2 -2
- package/lib/Local/RpcSerialization.js.map +1 -1
- package/lib/Planetscale/Branch.d.ts.map +1 -1
- package/lib/Planetscale/Branch.js +25 -3
- package/lib/Planetscale/Branch.js.map +1 -1
- package/lib/Planetscale/Database.d.ts +1 -1
- package/lib/Planetscale/Postgres/PostgresClusterSize.d.ts +1 -1
- package/lib/Planetscale/Postgres/PostgresClusterSize.d.ts.map +1 -1
- package/lib/Planetscale/Postgres/PostgresClusterSize.js +13 -3
- package/lib/Planetscale/Postgres/PostgresClusterSize.js.map +1 -1
- package/lib/Planetscale/Util.d.ts +1 -1
- package/lib/Planetscale/Util.d.ts.map +1 -1
- package/lib/Planetscale/Util.js +7 -5
- package/lib/Planetscale/Util.js.map +1 -1
- package/lib/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/Cloudflare/Hyperdrive/Hyperdrive.ts +6 -8
- package/src/Cloudflare/Local.ts +9 -5
- package/src/Cloudflare/LocalRuntime.ts +70 -0
- package/src/Cloudflare/Providers.ts +2 -0
- package/src/Cloudflare/Queue/Queue.ts +73 -2
- package/src/Cloudflare/Queue/QueueConsumer.ts +130 -28
- package/src/Cloudflare/Workers/Assets.ts +2 -2
- package/src/Cloudflare/Workers/LocalWorkerProvider.ts +39 -30
- package/src/Cloudflare/Workers/Worker.ts +5 -26
- package/src/Local/ProviderLayer.ts +22 -0
- package/src/Local/RpcProvider.ts +2 -0
- package/src/Local/RpcProviderProxy.ts +2 -2
- package/src/Local/RpcSerialization.ts +2 -2
- package/src/Planetscale/Branch.ts +38 -10
- package/src/Planetscale/Database.ts +1 -1
- package/src/Planetscale/Postgres/PostgresClusterSize.ts +16 -2
- package/src/Planetscale/Util.ts +7 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alchemy",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.54",
|
|
4
4
|
"homepage": "https://alchemy.run",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Sam Goodwin <sam@alchemy.run>",
|
|
@@ -294,9 +294,9 @@
|
|
|
294
294
|
"@distilled.cloud/aws": "^0.24.5",
|
|
295
295
|
"@distilled.cloud/axiom": "^0.24.5",
|
|
296
296
|
"@distilled.cloud/cloudflare": "^0.24.5",
|
|
297
|
-
"@distilled.cloud/cloudflare-rolldown-plugin": "0.10.
|
|
298
|
-
"@distilled.cloud/cloudflare-runtime": "0.10.
|
|
299
|
-
"@distilled.cloud/cloudflare-vite-plugin": "0.10.
|
|
297
|
+
"@distilled.cloud/cloudflare-rolldown-plugin": "0.10.12",
|
|
298
|
+
"@distilled.cloud/cloudflare-runtime": "0.10.12",
|
|
299
|
+
"@distilled.cloud/cloudflare-vite-plugin": "0.10.12",
|
|
300
300
|
"@distilled.cloud/core": "^0.24.5",
|
|
301
301
|
"@distilled.cloud/neon": "^0.24.5",
|
|
302
302
|
"@distilled.cloud/planetscale": "^0.24.5",
|
|
@@ -326,16 +326,16 @@
|
|
|
326
326
|
},
|
|
327
327
|
"devDependencies": {
|
|
328
328
|
"@clack/prompts": "^0.11.0",
|
|
329
|
-
"@cloudflare/workers-types": "^4.20250805.0",
|
|
330
329
|
"@cloudflare/puppeteer": "^1.1.0",
|
|
330
|
+
"@cloudflare/workers-types": "^4.20250805.0",
|
|
331
331
|
"@effect/platform-bun": ">=4.0.0-beta.78 || >=4.0.0",
|
|
332
332
|
"@effect/platform-node": ">=4.0.0-beta.78 || >=4.0.0",
|
|
333
333
|
"@effect/platform-node-shared": ">=4.0.0-beta.78 || >=4.0.0",
|
|
334
334
|
"@oxc-project/types": "^0.127.0",
|
|
335
|
-
"@types/pg": "^8.11.0",
|
|
336
335
|
"@types/aws-lambda": "^8.10.152",
|
|
337
336
|
"@types/bun": "latest",
|
|
338
337
|
"@types/node": "latest",
|
|
338
|
+
"@types/pg": "^8.11.0",
|
|
339
339
|
"@types/picomatch": "^4.0.0",
|
|
340
340
|
"@types/react": "^19.2.2",
|
|
341
341
|
"@types/ws": "^8.18.1",
|
|
@@ -9,6 +9,7 @@ import { createPhysicalName } from "../../PhysicalName.ts";
|
|
|
9
9
|
import * as Provider from "../../Provider.ts";
|
|
10
10
|
import { Resource } from "../../Resource.ts";
|
|
11
11
|
import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
|
|
12
|
+
import { generateLocalId, isLiveId } from "../LocalRuntime.ts";
|
|
12
13
|
import type { Providers } from "../Providers.ts";
|
|
13
14
|
import { HyperdriveBinding } from "./HyperdriveBinding.ts";
|
|
14
15
|
|
|
@@ -182,7 +183,7 @@ export const HyperdriveProvider = () =>
|
|
|
182
183
|
if (oldName !== name) {
|
|
183
184
|
return { action: "replace" } as const;
|
|
184
185
|
}
|
|
185
|
-
if (!
|
|
186
|
+
if (!isLiveId(output?.hyperdriveId)) {
|
|
186
187
|
return { action: "update" };
|
|
187
188
|
}
|
|
188
189
|
}),
|
|
@@ -193,7 +194,7 @@ export const HyperdriveProvider = () =>
|
|
|
193
194
|
return output;
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
if (
|
|
197
|
+
if (isLiveId(output?.hyperdriveId)) {
|
|
197
198
|
return yield* hyperdrive
|
|
198
199
|
.getConfig({
|
|
199
200
|
accountId: output.accountId,
|
|
@@ -248,7 +249,7 @@ export const HyperdriveProvider = () =>
|
|
|
248
249
|
const ctx = yield* AlchemyContext;
|
|
249
250
|
if (ctx.dev) {
|
|
250
251
|
return {
|
|
251
|
-
hyperdriveId: output?.hyperdriveId ??
|
|
252
|
+
hyperdriveId: output?.hyperdriveId ?? generateLocalId(),
|
|
252
253
|
name,
|
|
253
254
|
accountId: output?.accountId ?? accountId,
|
|
254
255
|
origin: news.origin,
|
|
@@ -268,7 +269,7 @@ export const HyperdriveProvider = () =>
|
|
|
268
269
|
// to update; otherwise we createConfig and fall back to "find by
|
|
269
270
|
// name then update" if Cloudflare reports the name is already in
|
|
270
271
|
// use (race or a cold-start adoption).
|
|
271
|
-
const synced =
|
|
272
|
+
const synced = isLiveId(output?.hyperdriveId)
|
|
272
273
|
? yield* hyperdrive.updateConfig({
|
|
273
274
|
accountId: output.accountId,
|
|
274
275
|
hyperdriveId: output.hyperdriveId,
|
|
@@ -304,7 +305,7 @@ export const HyperdriveProvider = () =>
|
|
|
304
305
|
};
|
|
305
306
|
}),
|
|
306
307
|
delete: Effect.fn(function* ({ output }) {
|
|
307
|
-
if (!
|
|
308
|
+
if (!isLiveId(output.hyperdriveId)) return;
|
|
308
309
|
|
|
309
310
|
yield* hyperdrive
|
|
310
311
|
.deleteConfig({
|
|
@@ -341,9 +342,6 @@ export const defaultPort = (scheme: HyperdriveScheme): number =>
|
|
|
341
342
|
const unwrap = (v: string | Redacted.Redacted<string>): string =>
|
|
342
343
|
Redacted.isRedacted(v) ? Redacted.value(v) : v;
|
|
343
344
|
|
|
344
|
-
const isHyperdriveId = (maybeId: string | undefined): maybeId is string =>
|
|
345
|
-
typeof maybeId === "string" && !maybeId.startsWith("dev:");
|
|
346
|
-
|
|
347
345
|
/**
|
|
348
346
|
* Build the request body shape that the distilled `createConfig`/`updateConfig`
|
|
349
347
|
* methods accept. Secrets are unwrapped here because the distilled TS types
|
package/src/Cloudflare/Local.ts
CHANGED
|
@@ -3,10 +3,10 @@ import * as RpcServer from "../Local/RpcServer.ts";
|
|
|
3
3
|
import { CloudflareAuth } from "./Auth/AuthProvider.ts";
|
|
4
4
|
import * as CloudflareEnvironment from "./CloudflareEnvironment.ts";
|
|
5
5
|
import * as Credentials from "./Credentials.ts";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "./Workers/LocalWorkerProvider.ts";
|
|
6
|
+
import { localRuntimeServices } from "./LocalRuntime.ts";
|
|
7
|
+
import { QueueProviderLocal } from "./Queue/Queue.ts";
|
|
8
|
+
import { QueueConsumerProviderLocal } from "./Queue/QueueConsumer.ts";
|
|
9
|
+
import { LocalWorkerProvider } from "./Workers/LocalWorkerProvider.ts";
|
|
10
10
|
|
|
11
11
|
const cloudflareServices = Layer.provide(
|
|
12
12
|
Layer.merge(
|
|
@@ -16,7 +16,11 @@ const cloudflareServices = Layer.provide(
|
|
|
16
16
|
CloudflareAuth,
|
|
17
17
|
);
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Layer.mergeAll(
|
|
20
|
+
LocalWorkerProvider(),
|
|
21
|
+
QueueProviderLocal(),
|
|
22
|
+
QueueConsumerProviderLocal(),
|
|
23
|
+
).pipe(
|
|
20
24
|
Layer.provide(localRuntimeServices()),
|
|
21
25
|
Layer.provide(cloudflareServices),
|
|
22
26
|
RpcServer.launch,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { layerRuntime } from "@distilled.cloud/cloudflare-runtime";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as MutableHashMap from "effect/MutableHashMap";
|
|
6
|
+
import * as Path from "effect/Path";
|
|
7
|
+
import { AlchemyContext } from "../AlchemyContext.ts";
|
|
8
|
+
import * as RpcProvider from "../Local/RpcProvider.ts";
|
|
9
|
+
import { CloudflareEnvironment } from "./CloudflareEnvironment.ts";
|
|
10
|
+
import type { Queue } from "./Queue/Queue.ts";
|
|
11
|
+
import type { QueueConsumer } from "./Queue/QueueConsumer.ts";
|
|
12
|
+
|
|
13
|
+
export const LOCAL_ENTRY_URL = import.meta.resolve(
|
|
14
|
+
// `import.meta.resolve(<string>)` is a runtime API — TypeScript's
|
|
15
|
+
// `rewriteRelativeImportExtensions` does NOT touch the string literal, so
|
|
16
|
+
// we have to pick the right extension ourselves. `import.meta.url` reflects
|
|
17
|
+
// the actual on-disk extension of *this* file (`.ts` when loaded from
|
|
18
|
+
// `src/` under Bun or vitest, `.js` when loaded from the compiled `lib/`
|
|
19
|
+
// under Node), which is exactly the signal we need.
|
|
20
|
+
import.meta.url.endsWith(".ts") ? "./Local.ts" : "./Local.js",
|
|
21
|
+
import.meta.url,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
export class LocalRuntimeState extends Context.Service<
|
|
25
|
+
LocalRuntimeState,
|
|
26
|
+
{
|
|
27
|
+
readonly queues: MutableHashMap.MutableHashMap<
|
|
28
|
+
Queue["Attributes"]["queueId"],
|
|
29
|
+
Queue["Attributes"]
|
|
30
|
+
>;
|
|
31
|
+
readonly queueConsumers: MutableHashMap.MutableHashMap<
|
|
32
|
+
QueueConsumer["Attributes"]["consumerId"],
|
|
33
|
+
QueueConsumer["Attributes"]
|
|
34
|
+
>;
|
|
35
|
+
}
|
|
36
|
+
>()("alchemy/cloudflare/LocalRuntimeState") {}
|
|
37
|
+
|
|
38
|
+
const LocalRuntimeStateLive = Layer.succeed(
|
|
39
|
+
LocalRuntimeState,
|
|
40
|
+
LocalRuntimeState.of({
|
|
41
|
+
queues: MutableHashMap.empty(),
|
|
42
|
+
queueConsumers: MutableHashMap.empty(),
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export const localRuntimeServices = () =>
|
|
47
|
+
RpcProvider.providerServicesEffect(
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const getEnv = yield* CloudflareEnvironment;
|
|
50
|
+
const { dotAlchemy } = yield* AlchemyContext;
|
|
51
|
+
const path = yield* Path.Path;
|
|
52
|
+
return Layer.merge(
|
|
53
|
+
LocalRuntimeStateLive,
|
|
54
|
+
layerRuntime({
|
|
55
|
+
api: {
|
|
56
|
+
accountId: getEnv.pipe(Effect.map((env) => env.accountId)),
|
|
57
|
+
},
|
|
58
|
+
storage: {
|
|
59
|
+
directory: path.join(dotAlchemy, "local"),
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export const isLocalId = (id: string | undefined): id is string =>
|
|
67
|
+
typeof id === "string" && id.startsWith("dev:");
|
|
68
|
+
export const isLiveId = (id: string | undefined): id is string =>
|
|
69
|
+
typeof id === "string" && !id.startsWith("dev:");
|
|
70
|
+
export const generateLocalId = (): string => `dev:${crypto.randomUUID()}`;
|
|
@@ -29,6 +29,7 @@ import * as Email from "./Email/index.ts";
|
|
|
29
29
|
import * as Hyperdrive from "./Hyperdrive/index.ts";
|
|
30
30
|
import * as Images from "./Images/index.ts";
|
|
31
31
|
import * as KV from "./KV/index.ts";
|
|
32
|
+
import { localRuntimeServices } from "./LocalRuntime.ts";
|
|
32
33
|
import * as Queue from "./Queue/index.ts";
|
|
33
34
|
import * as R2 from "./R2/index.ts";
|
|
34
35
|
import * as RateLimit from "./RateLimit/index.ts";
|
|
@@ -173,6 +174,7 @@ export const providers = () =>
|
|
|
173
174
|
RandomProvider(),
|
|
174
175
|
),
|
|
175
176
|
),
|
|
177
|
+
Layer.provideMerge(localRuntimeServices()),
|
|
176
178
|
Layer.provideMerge(Credentials.fromAuthProvider()),
|
|
177
179
|
Layer.provideMerge(CloudflareEnvironment.fromProfile()),
|
|
178
180
|
Layer.provideMerge(CloudflareAuth),
|
|
@@ -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
|
|
108
|
+
export const QueueProviderLive = () =>
|
|
100
109
|
Provider.succeed(Queue, {
|
|
101
|
-
|
|
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
|
-
|
|
131
|
+
|
|
132
|
+
export const QueueConsumerProviderLive = () =>
|
|
120
133
|
Provider.succeed(QueueConsumer, {
|
|
121
|
-
|
|
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
|
|
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 =
|
|
101
|
+
const matcher = createIgnore().add(patterns);
|
|
102
102
|
return (file: string) => matcher.ignores(file);
|
|
103
103
|
};
|
|
104
104
|
|