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
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
765
|
-
live: LiveWorkerProvider,
|
|
766
|
-
|
|
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
|
+
);
|
package/src/Local/RpcProvider.ts
CHANGED
|
@@ -76,6 +76,7 @@ export const effect = <
|
|
|
76
76
|
cls,
|
|
77
77
|
Effect.gen(function* () {
|
|
78
78
|
const client = yield* Effect.serviceOption(RpcProviderProxy);
|
|
79
|
+
const context = yield* Effect.context();
|
|
79
80
|
const stack = yield* Stack;
|
|
80
81
|
|
|
81
82
|
if (client._tag === "None") {
|
|
@@ -87,6 +88,7 @@ export const effect = <
|
|
|
87
88
|
return (...args: any[]) => {
|
|
88
89
|
const result = value(...args);
|
|
89
90
|
const services = Layer.mergeAll(
|
|
91
|
+
Layer.succeedContext(context),
|
|
90
92
|
layerFallback(Stack, stack),
|
|
91
93
|
layerFallback(Stage, stack.stage),
|
|
92
94
|
Predicate.hasProperty(args[0], "instanceId") &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { newWebSocketRpcSession
|
|
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)
|
|
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
|
]),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as planetscale from "@distilled.cloud/planetscale";
|
|
2
2
|
import { Credentials } from "@distilled.cloud/planetscale/Credentials";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Schedule from "effect/Schedule";
|
|
4
5
|
import { Unowned } from "../AdoptPolicy.ts";
|
|
5
6
|
import { isResolved } from "../Diff.ts";
|
|
6
7
|
import { createPhysicalName } from "../PhysicalName.ts";
|
|
@@ -157,6 +158,20 @@ export interface BranchMigrationRunners {
|
|
|
157
158
|
|
|
158
159
|
const rootDir = Effect.sync(() => process.cwd());
|
|
159
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Matches the UnprocessableEntity errors PlanetScale returns when a
|
|
163
|
+
* branch promotion/demotion races an in-flight cluster resize, e.g.
|
|
164
|
+
* "Branches with a cluster resize in progress cannot be demoted."
|
|
165
|
+
*/
|
|
166
|
+
const isResizeInProgress = (error: unknown): boolean =>
|
|
167
|
+
typeof error === "object" &&
|
|
168
|
+
error !== null &&
|
|
169
|
+
(error as { readonly _tag?: unknown })._tag === "UnprocessableEntity" &&
|
|
170
|
+
typeof (error as { readonly message?: unknown }).message === "string" &&
|
|
171
|
+
(error as { readonly message: string }).message.includes(
|
|
172
|
+
"cluster resize in progress",
|
|
173
|
+
);
|
|
174
|
+
|
|
160
175
|
/**
|
|
161
176
|
* Build a branch provider for a specific PlanetScale engine. The
|
|
162
177
|
* caller supplies the typed `Resource` token and the engine's migration
|
|
@@ -369,17 +384,30 @@ export const makeBranchProvider = <R extends ResourceLike>(opts: {
|
|
|
369
384
|
if (opts.expectedKind === "mysql") {
|
|
370
385
|
const desiredProduction = news.isProduction ?? false;
|
|
371
386
|
if (current.production !== desiredProduction) {
|
|
387
|
+
// A keyspace can report `resizing: false` while the resize
|
|
388
|
+
// workflow is still finalizing, during which promote/demote is
|
|
389
|
+
// rejected with an UnprocessableEntity. Retry until it clears.
|
|
390
|
+
const retryWhileResizing = Effect.retry({
|
|
391
|
+
while: isResizeInProgress,
|
|
392
|
+
schedule: Schedule.spaced("5 seconds").pipe(
|
|
393
|
+
Schedule.both(Schedule.recurs(120)),
|
|
394
|
+
),
|
|
395
|
+
});
|
|
372
396
|
current = desiredProduction
|
|
373
|
-
? yield* planetscale
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
397
|
+
? yield* planetscale
|
|
398
|
+
.promoteBranch({
|
|
399
|
+
organization,
|
|
400
|
+
database: databaseName,
|
|
401
|
+
branch: branchName,
|
|
402
|
+
})
|
|
403
|
+
.pipe(retryWhileResizing)
|
|
404
|
+
: yield* planetscale
|
|
405
|
+
.demoteBranch({
|
|
406
|
+
organization,
|
|
407
|
+
database: databaseName,
|
|
408
|
+
branch: branchName,
|
|
409
|
+
})
|
|
410
|
+
.pipe(retryWhileResizing);
|
|
383
411
|
}
|
|
384
412
|
}
|
|
385
413
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as planetscale from "@distilled.cloud/planetscale";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
|
-
import
|
|
3
|
+
import * as Schedule from "effect/Schedule";
|
|
4
|
+
import { pollUntil, waitForBranchReady } from "../Util.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Available PlanetScale PostgreSQL cluster sizes.
|
|
@@ -54,6 +55,15 @@ export function toPostgresClusterSku(input: {
|
|
|
54
55
|
return `${size}_${provider}_${arch}`;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Schedule for polling branch change requests. Postgres cluster resizes
|
|
60
|
+
* routinely take longer than the default 10-minute polling budget, so
|
|
61
|
+
* give change requests a 60-minute budget (720 × 5s).
|
|
62
|
+
*/
|
|
63
|
+
const changeRequestSchedule = Schedule.spaced("5 seconds").pipe(
|
|
64
|
+
Schedule.both(Schedule.recurs(720)),
|
|
65
|
+
);
|
|
66
|
+
|
|
57
67
|
/**
|
|
58
68
|
* Polls branch change requests until all visible changes are in a terminal
|
|
59
69
|
* state (`completed` or `canceled`), or — if `changeId` is provided — until
|
|
@@ -85,6 +95,7 @@ export const waitForPendingPostgresChanges = Effect.fn(function* (
|
|
|
85
95
|
|
|
86
96
|
return page.data.every((change) => isTerminal(change.state));
|
|
87
97
|
},
|
|
98
|
+
changeRequestSchedule,
|
|
88
99
|
);
|
|
89
100
|
});
|
|
90
101
|
|
|
@@ -98,7 +109,10 @@ export const ensurePostgresProductionBranchClusterSize = Effect.fn(function* (
|
|
|
98
109
|
branch: string,
|
|
99
110
|
expectedClusterSize: PostgresClusterSize,
|
|
100
111
|
) {
|
|
101
|
-
|
|
112
|
+
// A freshly-forked branch can't accept (or complete) change requests
|
|
113
|
+
// until it has finished provisioning — queueing a resize against it
|
|
114
|
+
// just leaves the change pending until the poll budget runs out.
|
|
115
|
+
const data = yield* waitForBranchReady(organization, database, branch);
|
|
102
116
|
|
|
103
117
|
const sku = toPostgresClusterSku({
|
|
104
118
|
size: expectedClusterSize,
|
package/src/Planetscale/Util.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const DEFAULT_MIGRATIONS_TABLE = "__alchemy_migrations";
|
|
|
12
12
|
* been reached. Used internally with `Effect.retry` to drive long-running
|
|
13
13
|
* status waits.
|
|
14
14
|
*/
|
|
15
|
-
class NotReady extends Data.TaggedError("Planetscale::NotReady")<{
|
|
15
|
+
export class NotReady extends Data.TaggedError("Planetscale::NotReady")<{
|
|
16
16
|
description: string;
|
|
17
17
|
}> {}
|
|
18
18
|
|
|
@@ -29,12 +29,14 @@ export class PlanetscaleConflict extends Data.TaggedError(
|
|
|
29
29
|
}> {}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Default polling schedule: 5s spaced retries with a
|
|
33
|
-
* budget (
|
|
34
|
-
* iterations would wait hours, indistinguishable from a hang.
|
|
32
|
+
* Default polling schedule: 5s spaced retries with a 30-minute total
|
|
33
|
+
* budget (360 × 5s). Avoids the exponential-blowup trap where later
|
|
34
|
+
* iterations would wait hours, indistinguishable from a hang. Postgres
|
|
35
|
+
* database creates routinely run 10-12 minutes, so a 10-minute budget
|
|
36
|
+
* regularly false-positives as "stuck".
|
|
35
37
|
*/
|
|
36
38
|
const defaultSchedule = Schedule.spaced("5 seconds").pipe(
|
|
37
|
-
Schedule.both(Schedule.recurs(
|
|
39
|
+
Schedule.both(Schedule.recurs(360)),
|
|
38
40
|
);
|
|
39
41
|
|
|
40
42
|
/**
|