@gaganref/confect-alchemy 0.0.0-stage → 10.3.1-fork.2
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/LICENSE +7 -0
- package/README.md +22 -2
- package/dist/Backend.d.ts +45 -0
- package/dist/Backend.d.ts.map +1 -0
- package/dist/Backend.js +38 -0
- package/dist/Backend.js.map +1 -0
- package/dist/Code.d.ts +80 -0
- package/dist/Code.d.ts.map +1 -0
- package/dist/Code.js +29 -0
- package/dist/Code.js.map +1 -0
- package/dist/Confect.d.ts +2 -0
- package/dist/Confect.d.ts.map +1 -0
- package/dist/Confect.js +8 -0
- package/dist/Confect.js.map +1 -0
- package/dist/Convex.d.ts +7 -0
- package/dist/Convex.d.ts.map +1 -0
- package/dist/Convex.js +21 -0
- package/dist/Convex.js.map +1 -0
- package/dist/ConvexClient.d.ts +215 -0
- package/dist/ConvexClient.d.ts.map +1 -0
- package/dist/ConvexClient.js +224 -0
- package/dist/ConvexClient.js.map +1 -0
- package/dist/DeployKey.d.ts +120 -0
- package/dist/DeployKey.d.ts.map +1 -0
- package/dist/DeployKey.js +151 -0
- package/dist/DeployKey.js.map +1 -0
- package/dist/Deployment.d.ts +113 -0
- package/dist/Deployment.d.ts.map +1 -0
- package/dist/Deployment.js +160 -0
- package/dist/Deployment.js.map +1 -0
- package/dist/EnvironmentVariables.d.ts +155 -0
- package/dist/EnvironmentVariables.d.ts.map +1 -0
- package/dist/EnvironmentVariables.js +135 -0
- package/dist/EnvironmentVariables.js.map +1 -0
- package/dist/Project.d.ts +100 -0
- package/dist/Project.d.ts.map +1 -0
- package/dist/Project.js +123 -0
- package/dist/Project.js.map +1 -0
- package/dist/Providers.d.ts +17 -0
- package/dist/Providers.d.ts.map +1 -0
- package/dist/Providers.js +46 -0
- package/dist/Providers.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/internal/CodeDeployment.d.ts +17 -0
- package/dist/internal/CodeDeployment.d.ts.map +1 -0
- package/dist/internal/CodeDeployment.js +53 -0
- package/dist/internal/CodeDeployment.js.map +1 -0
- package/dist/internal/CommandRunner.d.ts +55 -0
- package/dist/internal/CommandRunner.d.ts.map +1 -0
- package/dist/internal/CommandRunner.js +126 -0
- package/dist/internal/CommandRunner.js.map +1 -0
- package/dist/internal/FileLock.d.ts +30 -0
- package/dist/internal/FileLock.d.ts.map +1 -0
- package/dist/internal/FileLock.js +83 -0
- package/dist/internal/FileLock.js.map +1 -0
- package/dist/internal/Lifecycle.d.ts +11 -0
- package/dist/internal/Lifecycle.d.ts.map +1 -0
- package/dist/internal/Lifecycle.js +45 -0
- package/dist/internal/Lifecycle.js.map +1 -0
- package/dist/internal/LocalBackend.d.ts +63 -0
- package/dist/internal/LocalBackend.d.ts.map +1 -0
- package/dist/internal/LocalBackend.js +333 -0
- package/dist/internal/LocalBackend.js.map +1 -0
- package/dist/internal/LocalSidecar.d.ts +3 -0
- package/dist/internal/LocalSidecar.d.ts.map +1 -0
- package/dist/internal/LocalSidecar.js +7 -0
- package/dist/internal/LocalSidecar.js.map +1 -0
- package/dist/internal/Loopback.d.ts +18 -0
- package/dist/internal/Loopback.d.ts.map +1 -0
- package/dist/internal/Loopback.js +31 -0
- package/dist/internal/Loopback.js.map +1 -0
- package/dist/internal/PushLock.d.ts +36 -0
- package/dist/internal/PushLock.d.ts.map +1 -0
- package/dist/internal/PushLock.js +61 -0
- package/dist/internal/PushLock.js.map +1 -0
- package/dist/internal/ResourceIdentity.d.ts +3 -0
- package/dist/internal/ResourceIdentity.d.ts.map +1 -0
- package/dist/internal/ResourceIdentity.js +10 -0
- package/dist/internal/ResourceIdentity.js.map +1 -0
- package/dist/internal/UserCache.d.ts +12 -0
- package/dist/internal/UserCache.d.ts.map +1 -0
- package/dist/internal/UserCache.js +25 -0
- package/dist/internal/UserCache.js.map +1 -0
- package/dist/internal/Zip.d.ts +15 -0
- package/dist/internal/Zip.d.ts.map +1 -0
- package/dist/internal/Zip.js +65 -0
- package/dist/internal/Zip.js.map +1 -0
- package/package.json +65 -3
- package/src/Backend.ts +101 -0
- package/src/Code.ts +55 -0
- package/src/Confect.ts +1 -0
- package/src/Convex.ts +18 -0
- package/src/ConvexClient.ts +544 -0
- package/src/DeployKey.ts +256 -0
- package/src/Deployment.ts +277 -0
- package/src/EnvironmentVariables.ts +254 -0
- package/src/Project.ts +205 -0
- package/src/Providers.ts +67 -0
- package/src/index.ts +2 -0
- package/src/internal/CodeDeployment.ts +81 -0
- package/src/internal/CommandRunner.ts +251 -0
- package/src/internal/FileLock.ts +136 -0
- package/src/internal/Lifecycle.ts +49 -0
- package/src/internal/LocalBackend.ts +492 -0
- package/src/internal/LocalSidecar.ts +9 -0
- package/src/internal/Loopback.ts +34 -0
- package/src/internal/PushLock.ts +99 -0
- package/src/internal/ResourceIdentity.ts +20 -0
- package/src/internal/UserCache.ts +25 -0
- package/src/internal/Zip.ts +108 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CommandRunner.js","names":[],"sources":["../../src/internal/CommandRunner.ts"],"sourcesContent":["import * as Config from \"effect/Config\";\nimport * as Context from \"effect/Context\";\nimport * as Effect from \"effect/Effect\";\nimport * as FileSystem from \"effect/FileSystem\";\nimport * as Layer from \"effect/Layer\";\nimport * as Path from \"effect/Path\";\nimport * as Redacted from \"effect/Redacted\";\nimport * as Schema from \"effect/Schema\";\nimport { ChildProcess, ChildProcessSpawner } from \"effect/process\";\n\nexport interface PrepareCommand {\n readonly command: string;\n readonly args: ReadonlyArray<string>;\n}\n\n/**\n * Where a push goes: a cloud deployment through its deploy key, or a local\n * backend through the CLI's self-hosted target and its admin key.\n */\nexport type PushTarget =\n | {\n readonly _tag: \"Cloud\";\n readonly deploymentName: string;\n readonly deployKey: Redacted.Redacted<string>;\n }\n | {\n readonly _tag: \"SelfHosted\";\n readonly deploymentName: string;\n readonly url: string;\n readonly adminKey: Redacted.Redacted<string>;\n };\n\nconst selection = (target: PushTarget) =>\n target._tag === \"Cloud\"\n ? `CONVEX_DEPLOY_KEY=${Redacted.value(target.deployKey)}\\n`\n : `CONVEX_SELF_HOSTED_URL=${target.url}\\nCONVEX_SELF_HOSTED_ADMIN_KEY=${Redacted.value(target.adminKey)}\\n`;\n\nexport class CommandError extends Schema.TaggedError<CommandError>()(\n \"CommandError\",\n {\n operation: Schema.Literals([\"prepare\", \"deploy\"]),\n reason: Schema.Literals([\"configuration\", \"spawn\", \"exit\"]),\n exitCode: Schema.optionalKey(Schema.Finite),\n },\n) {}\n\nexport class CommandRunner extends Context.Service<\n CommandRunner,\n {\n readonly prepare: (\n command: PrepareCommand,\n cwd?: string,\n ) => Effect.Effect<void, CommandError>;\n readonly deploy: (\n target: PushTarget,\n cwd?: string,\n ) => Effect.Effect<void, CommandError>;\n }\n>()(\"@confect/alchemy/internal/CommandRunner\") {}\n\n/**\n * The ambient variables a child process needs to run on every platform; the\n * rest of the parent environment, credentials included, is withheld.\n */\nexport const processEnvironment = Config.all({\n PATH: Config.String(\"PATH\").pipe(\n Config.orElse(() => Config.String(\"Path\")),\n Config.withDefault(\"\"),\n ),\n HOME: Config.String(\"HOME\").pipe(Config.withDefault(\"\")),\n USERPROFILE: Config.String(\"USERPROFILE\").pipe(Config.withDefault(\"\")),\n SystemRoot: Config.String(\"SystemRoot\").pipe(\n Config.orElse(() => Config.String(\"SYSTEMROOT\")),\n Config.withDefault(\"\"),\n ),\n TMPDIR: Config.String(\"TMPDIR\").pipe(Config.withDefault(\"\")),\n TEMP: Config.String(\"TEMP\").pipe(Config.withDefault(\"\")),\n});\n\nconst packageBin = Effect.fn(\"CommandRunner.packageBin\")(function* (\n fs: FileSystem.FileSystem,\n path: Path.Path,\n cwd: string,\n packageName: string,\n binName: string,\n) {\n const invalid = () =>\n new CommandError({\n operation: packageName === \"convex\" ? \"deploy\" : \"prepare\",\n reason: \"configuration\",\n });\n let directory = cwd;\n while (true) {\n const manifestPath = path.join(\n directory,\n \"node_modules\",\n packageName,\n \"package.json\",\n );\n if (yield* fs.exists(manifestPath).pipe(Effect.mapError(invalid))) {\n const manifest = yield* fs.readFileString(manifestPath).pipe(\n Effect.flatMap(\n Schema.decodeEffect(\n Schema.fromJsonString(\n Schema.Struct({\n bin: Schema.Union([\n Schema.String,\n Schema.Record(Schema.String, Schema.String),\n ]),\n }),\n ),\n ),\n ),\n Effect.mapError(invalid),\n );\n const relative =\n typeof manifest.bin === \"string\" ? manifest.bin : manifest.bin[binName];\n if (!relative) return yield* invalid();\n return path.resolve(path.dirname(manifestPath), relative);\n }\n const parent = path.dirname(directory);\n if (parent === directory) return yield* invalid();\n directory = parent;\n }\n});\n\nexport const layer = Layer.effect(\n CommandRunner,\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;\n const run = Effect.fn(\"CommandRunner.run\")(function* (\n operation: \"prepare\" | \"deploy\",\n command: string,\n args: ReadonlyArray<string>,\n cwd: string,\n env: Record<string, string | undefined>,\n ) {\n const code = yield* spawner\n .exitCode(\n ChildProcess.make(command, args, {\n cwd,\n env,\n extendEnv: false,\n stdin: \"ignore\",\n stdout: \"ignore\",\n stderr: \"ignore\",\n }),\n )\n .pipe(\n Effect.mapError(\n () => new CommandError({ operation, reason: \"spawn\" }),\n ),\n );\n if (code !== 0)\n return yield* new CommandError({\n operation,\n reason: \"exit\",\n exitCode: Number(code),\n });\n });\n const environment = processEnvironment.pipe(\n Effect.map((env) => ({ ...env, CI: \"true\", DO_NOT_TRACK: \"1\" })),\n );\n return CommandRunner.of({\n prepare: Effect.fn(\"CommandRunner.prepare\")(function* (\n command,\n cwd = process.cwd(),\n ) {\n const directory = path.resolve(cwd);\n const env = yield* environment.pipe(\n Effect.mapError(\n () =>\n new CommandError({\n operation: \"prepare\",\n reason: \"configuration\",\n }),\n ),\n );\n if (command.command === \"confect\") {\n const bin = yield* packageBin(\n fs,\n path,\n directory,\n \"@confect/cli\",\n \"confect\",\n );\n yield* run(\n \"prepare\",\n process.execPath,\n [bin, ...command.args],\n directory,\n env,\n );\n } else {\n yield* run(\"prepare\", command.command, command.args, directory, env);\n }\n }),\n deploy: Effect.fn(\"CommandRunner.deploy\")(function* (\n target,\n cwd = process.cwd(),\n ) {\n const directory = path.resolve(cwd);\n const bin = yield* packageBin(fs, path, directory, \"convex\", \"convex\");\n const env = yield* environment.pipe(\n Effect.mapError(\n () =>\n new CommandError({\n operation: \"deploy\",\n reason: \"configuration\",\n }),\n ),\n );\n yield* Effect.scoped(\n Effect.gen(function* () {\n const temp = yield* fs.makeTempDirectoryScoped({\n prefix: \"confect-deploy-\",\n });\n const envFile = path.join(temp, \"deployment.env\");\n yield* fs.writeFileString(envFile, selection(target), {\n mode: 0o600,\n });\n yield* run(\n \"deploy\",\n process.execPath,\n [bin, \"deploy\", \"--yes\", \"--env-file\", envFile],\n directory,\n {\n ...env,\n HOME: temp,\n USERPROFILE: temp,\n LOCALAPPDATA: temp,\n XDG_CONFIG_HOME: temp,\n },\n );\n }).pipe(\n Effect.mapError((error) =>\n Schema.is(CommandError)(error)\n ? error\n : new CommandError({\n operation: \"deploy\",\n reason: \"configuration\",\n }),\n ),\n ),\n );\n }),\n });\n }),\n);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as Crypto from "effect/Crypto";
|
|
2
|
+
import type * as Duration from "effect/Duration";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as FileSystem from "effect/FileSystem";
|
|
5
|
+
import * as Path from "effect/Path";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
declare const FileLockError_base: Schema.Class<FileLockError, Schema.TaggedStruct<"FileLockError", {
|
|
8
|
+
readonly reason: Schema.Literals<readonly ["held", "timeout", "filesystem"]>;
|
|
9
|
+
}>, import("effect/Cause").YieldableError>;
|
|
10
|
+
export declare class FileLockError extends FileLockError_base {
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Hold an exclusive lock file for the ambient scope, waiting up to `wait` for
|
|
14
|
+
* another holder or failing with `held` at once when `wait` is omitted.
|
|
15
|
+
*
|
|
16
|
+
* The holder, `{ pid, token }`, is written to a private claim file and
|
|
17
|
+
* published with a hard link, which fails when the lock exists, so a visible
|
|
18
|
+
* lock always names its holder. A lock whose holder has exited is removed only
|
|
19
|
+
* under `<file>.recovery`, after re-reading that the same dead holder still
|
|
20
|
+
* owns it, so a successor's lock is never removed in its place. Publishing and
|
|
21
|
+
* recovering are uninterruptible: Node cannot cancel a link or unlink it has
|
|
22
|
+
* started, so stopping early would let one land after `.recovery` is released.
|
|
23
|
+
* The recovery lock is never reclaimed: a crash inside its few file operations
|
|
24
|
+
* leaves waiters timing out rather than overlapping. A timeout that lands while
|
|
25
|
+
* an attempt succeeds still fails, and the published lock is removed only when
|
|
26
|
+
* the ambient scope closes.
|
|
27
|
+
*/
|
|
28
|
+
export declare const acquire: (file: string, wait?: Duration.Input | undefined) => Effect.Effect<undefined, FileLockError, Crypto.Crypto | FileSystem.FileSystem | Path.Path | import("effect/Scope").Scope>;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=FileLock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileLock.d.ts","sourceRoot":"","sources":["../../src/internal/FileLock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,IAAI,MAAM,aAAa,CAAC;AAGpC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;;;;AAExC,qBAAa,aAAc,SAAQ,kBAGlC;CAAG;AAgBJ;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,OAAO,gLA0FlB,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import * as FileSystem from "effect/FileSystem";
|
|
4
|
+
import * as Path from "effect/Path";
|
|
5
|
+
import * as Option from "effect/Option";
|
|
6
|
+
import * as Crypto from "effect/Crypto";
|
|
7
|
+
import * as Predicate from "effect/Predicate";
|
|
8
|
+
import * as Schedule from "effect/Schedule";
|
|
9
|
+
//#region src/internal/FileLock.ts
|
|
10
|
+
var FileLockError = class extends Schema.TaggedError()("FileLockError", { reason: Schema.Literals([
|
|
11
|
+
"held",
|
|
12
|
+
"timeout",
|
|
13
|
+
"filesystem"
|
|
14
|
+
]) }) {};
|
|
15
|
+
const Holder = Schema.fromJsonString(Schema.Struct({
|
|
16
|
+
pid: Schema.Finite,
|
|
17
|
+
token: Schema.String
|
|
18
|
+
}));
|
|
19
|
+
const decodeHolder = Schema.decodeUnknownOption(Holder);
|
|
20
|
+
const isAlive = (pid) => {
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return Predicate.hasProperty(error, "code") && error.code === "EPERM";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Hold an exclusive lock file for the ambient scope, waiting up to `wait` for
|
|
30
|
+
* another holder or failing with `held` at once when `wait` is omitted.
|
|
31
|
+
*
|
|
32
|
+
* The holder, `{ pid, token }`, is written to a private claim file and
|
|
33
|
+
* published with a hard link, which fails when the lock exists, so a visible
|
|
34
|
+
* lock always names its holder. A lock whose holder has exited is removed only
|
|
35
|
+
* under `<file>.recovery`, after re-reading that the same dead holder still
|
|
36
|
+
* owns it, so a successor's lock is never removed in its place. Publishing and
|
|
37
|
+
* recovering are uninterruptible: Node cannot cancel a link or unlink it has
|
|
38
|
+
* started, so stopping early would let one land after `.recovery` is released.
|
|
39
|
+
* The recovery lock is never reclaimed: a crash inside its few file operations
|
|
40
|
+
* leaves waiters timing out rather than overlapping. A timeout that lands while
|
|
41
|
+
* an attempt succeeds still fails, and the published lock is removed only when
|
|
42
|
+
* the ambient scope closes.
|
|
43
|
+
*/
|
|
44
|
+
const acquire = Effect.fn("FileLock.acquire")(function* (file, wait) {
|
|
45
|
+
const fs = yield* FileSystem.FileSystem;
|
|
46
|
+
const path = yield* Path.Path;
|
|
47
|
+
const crypto = yield* Crypto.Crypto;
|
|
48
|
+
const failed = () => new FileLockError({ reason: "filesystem" });
|
|
49
|
+
const token = Buffer.from(yield* crypto.randomBytes(16).pipe(Effect.mapError(failed))).toString("hex");
|
|
50
|
+
const holder = yield* Schema.encodeEffect(Holder)({
|
|
51
|
+
pid: process.pid,
|
|
52
|
+
token
|
|
53
|
+
}).pipe(Effect.mapError(failed));
|
|
54
|
+
const claim = `${file}.${token}`;
|
|
55
|
+
const recovery = `${file}.recovery`;
|
|
56
|
+
const removeIfOwned = (target) => fs.readFileString(target).pipe(Effect.flatMap((content) => content === holder ? fs.remove(target) : Effect.void), Effect.ignore);
|
|
57
|
+
yield* Effect.addFinalizer(() => Effect.all([removeIfOwned(file), fs.remove(claim, { force: true }).pipe(Effect.ignore)]));
|
|
58
|
+
yield* fs.makeDirectory(path.dirname(file), { recursive: true }).pipe(Effect.mapError(failed));
|
|
59
|
+
yield* fs.writeFileString(claim, holder, { mode: 384 }).pipe(Effect.mapError(failed));
|
|
60
|
+
const read = (target) => fs.readFileString(target).pipe(Effect.map(decodeHolder), Effect.orElseSucceed(() => Option.none()));
|
|
61
|
+
const publish = (target) => fs.link(claim, target).pipe(Effect.as(true), Effect.catchReason("PlatformError", "AlreadyExists", () => Effect.succeed(false)));
|
|
62
|
+
const reclaim = Effect.gen(function* () {
|
|
63
|
+
const stale = yield* read(file);
|
|
64
|
+
if (Option.isNone(stale) || isAlive(stale.value.pid)) return false;
|
|
65
|
+
if (!(yield* publish(recovery))) return false;
|
|
66
|
+
return yield* read(file).pipe(Effect.flatMap((current) => Option.isSome(current) && current.value.token === stale.value.token ? fs.remove(file).pipe(Effect.as(true)) : Effect.succeed(false)), Effect.ensuring(removeIfOwned(recovery)));
|
|
67
|
+
});
|
|
68
|
+
const attempt = publish(file).pipe(Effect.flatMap((acquired) => acquired ? Effect.succeed(true) : Effect.flatMap(reclaim, (removed) => removed ? publish(file) : Effect.succeed(false))), Effect.uninterruptible, Effect.mapError(failed));
|
|
69
|
+
if (wait === void 0) {
|
|
70
|
+
if (!(yield* attempt)) return yield* new FileLockError({ reason: "held" });
|
|
71
|
+
} else yield* attempt.pipe(Effect.repeat({
|
|
72
|
+
until: (acquired) => acquired,
|
|
73
|
+
schedule: Schedule.spaced("100 millis")
|
|
74
|
+
}), Effect.timeoutOrElse({
|
|
75
|
+
duration: wait,
|
|
76
|
+
orElse: () => Effect.fail(new FileLockError({ reason: "timeout" }))
|
|
77
|
+
}));
|
|
78
|
+
yield* fs.remove(claim, { force: true }).pipe(Effect.ignore);
|
|
79
|
+
});
|
|
80
|
+
//#endregion
|
|
81
|
+
export { FileLockError, acquire };
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=FileLock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FileLock.js","names":[],"sources":["../../src/internal/FileLock.ts"],"sourcesContent":["import * as Crypto from \"effect/Crypto\";\nimport type * as Duration from \"effect/Duration\";\nimport * as Effect from \"effect/Effect\";\nimport * as FileSystem from \"effect/FileSystem\";\nimport * as Option from \"effect/Option\";\nimport * as Path from \"effect/Path\";\nimport * as Predicate from \"effect/Predicate\";\nimport * as Schedule from \"effect/Schedule\";\nimport * as Schema from \"effect/Schema\";\n\nexport class FileLockError extends Schema.TaggedError<FileLockError>()(\n \"FileLockError\",\n { reason: Schema.Literals([\"held\", \"timeout\", \"filesystem\"]) },\n) {}\n\nconst Holder = Schema.fromJsonString(\n Schema.Struct({ pid: Schema.Finite, token: Schema.String }),\n);\nconst decodeHolder = Schema.decodeUnknownOption(Holder);\n\nconst isAlive = (pid: number) => {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return Predicate.hasProperty(error, \"code\") && error.code === \"EPERM\";\n }\n};\n\n/**\n * Hold an exclusive lock file for the ambient scope, waiting up to `wait` for\n * another holder or failing with `held` at once when `wait` is omitted.\n *\n * The holder, `{ pid, token }`, is written to a private claim file and\n * published with a hard link, which fails when the lock exists, so a visible\n * lock always names its holder. A lock whose holder has exited is removed only\n * under `<file>.recovery`, after re-reading that the same dead holder still\n * owns it, so a successor's lock is never removed in its place. Publishing and\n * recovering are uninterruptible: Node cannot cancel a link or unlink it has\n * started, so stopping early would let one land after `.recovery` is released.\n * The recovery lock is never reclaimed: a crash inside its few file operations\n * leaves waiters timing out rather than overlapping. A timeout that lands while\n * an attempt succeeds still fails, and the published lock is removed only when\n * the ambient scope closes.\n */\nexport const acquire = Effect.fn(\"FileLock.acquire\")(function* (\n file: string,\n wait?: Duration.Input,\n) {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n const crypto = yield* Crypto.Crypto;\n const failed = () => new FileLockError({ reason: \"filesystem\" });\n const token = Buffer.from(\n yield* crypto.randomBytes(16).pipe(Effect.mapError(failed)),\n ).toString(\"hex\");\n const holder = yield* Schema.encodeEffect(Holder)({\n pid: process.pid,\n token,\n }).pipe(Effect.mapError(failed));\n const claim = `${file}.${token}`;\n const recovery = `${file}.recovery`;\n const removeIfOwned = (target: string) =>\n fs.readFileString(target).pipe(\n Effect.flatMap((content) =>\n content === holder ? fs.remove(target) : Effect.void,\n ),\n Effect.ignore,\n );\n yield* Effect.addFinalizer(() =>\n Effect.all([\n removeIfOwned(file),\n fs.remove(claim, { force: true }).pipe(Effect.ignore),\n ]),\n );\n yield* fs\n .makeDirectory(path.dirname(file), { recursive: true })\n .pipe(Effect.mapError(failed));\n yield* fs\n .writeFileString(claim, holder, { mode: 0o600 })\n .pipe(Effect.mapError(failed));\n\n const read = (target: string) =>\n fs.readFileString(target).pipe(\n Effect.map(decodeHolder),\n Effect.orElseSucceed(() => Option.none()),\n );\n const publish = (target: string) =>\n fs.link(claim, target).pipe(\n Effect.as(true),\n Effect.catchReason(\"PlatformError\", \"AlreadyExists\", () =>\n Effect.succeed(false),\n ),\n );\n // Whether a dead holder's lock was removed.\n const reclaim = Effect.gen(function* () {\n const stale = yield* read(file);\n if (Option.isNone(stale) || isAlive(stale.value.pid)) return false;\n if (!(yield* publish(recovery))) return false;\n return yield* read(file).pipe(\n Effect.flatMap((current) =>\n Option.isSome(current) && current.value.token === stale.value.token\n ? fs.remove(file).pipe(Effect.as(true))\n : Effect.succeed(false),\n ),\n Effect.ensuring(removeIfOwned(recovery)),\n );\n });\n const attempt = publish(file).pipe(\n Effect.flatMap((acquired) =>\n acquired\n ? Effect.succeed(true)\n : Effect.flatMap(reclaim, (removed) =>\n removed ? publish(file) : Effect.succeed(false),\n ),\n ),\n Effect.uninterruptible,\n Effect.mapError(failed),\n );\n\n if (wait === undefined) {\n if (!(yield* attempt)) return yield* new FileLockError({ reason: \"held\" });\n } else {\n yield* attempt.pipe(\n Effect.repeat({\n until: (acquired) => acquired,\n schedule: Schedule.spaced(\"100 millis\"),\n }),\n Effect.timeoutOrElse({\n duration: wait,\n orElse: () => Effect.fail(new FileLockError({ reason: \"timeout\" })),\n }),\n );\n }\n yield* fs.remove(claim, { force: true }).pipe(Effect.ignore);\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
declare const LifecycleStateError_base: Schema.Class<LifecycleStateError, Schema.TaggedStruct<"LifecycleStateError", {
|
|
4
|
+
readonly fqn: Schema.String;
|
|
5
|
+
}>, import("effect/Cause").YieldableError>;
|
|
6
|
+
export declare class LifecycleStateError extends LifecycleStateError_base {
|
|
7
|
+
}
|
|
8
|
+
export declare const checkpointCreate: (fqn: string, instanceId: string, props: object, output?: object | undefined) => Effect.Effect<undefined, LifecycleStateError | import("alchemy/State/State").StateStoreError, never>;
|
|
9
|
+
export declare const shouldAdopt: (fqn: string) => Effect.Effect<boolean, never, never>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=Lifecycle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Lifecycle.d.ts","sourceRoot":"","sources":["../../src/internal/Lifecycle.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;;;;AAExC,qBAAa,mBAAoB,SAAQ,wBAGxC;CAAG;AAEJ,eAAO,MAAM,gBAAgB,uLAqB5B,CAAC;AAEF,eAAO,MAAM,WAAW,uDAYtB,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import { AdoptPolicy } from "alchemy/AdoptPolicy";
|
|
5
|
+
import { AlchemyContext } from "alchemy/AlchemyContext";
|
|
6
|
+
import { Stack } from "alchemy/Stack";
|
|
7
|
+
import { State } from "alchemy/State/State";
|
|
8
|
+
//#region src/internal/Lifecycle.ts
|
|
9
|
+
var LifecycleStateError = class extends Schema.TaggedError()("LifecycleStateError", { fqn: Schema.String }) {};
|
|
10
|
+
const checkpointCreate = Effect.fn("Lifecycle.checkpointCreate")(function* (fqn, instanceId, props, output) {
|
|
11
|
+
const stack = yield* Effect.serviceOption(Stack);
|
|
12
|
+
if (Option.isNone(stack)) return;
|
|
13
|
+
const state = yield* Effect.serviceOption(State);
|
|
14
|
+
if (Option.isNone(state)) return yield* new LifecycleStateError({ fqn });
|
|
15
|
+
const store = yield* state.value;
|
|
16
|
+
const key = {
|
|
17
|
+
stack: stack.value.name,
|
|
18
|
+
stage: stack.value.stage,
|
|
19
|
+
fqn
|
|
20
|
+
};
|
|
21
|
+
const row = yield* store.get(key);
|
|
22
|
+
if (!row || row.kind === "action" || row.instanceId !== instanceId) return yield* new LifecycleStateError({ fqn });
|
|
23
|
+
if (row.status !== "creating") return;
|
|
24
|
+
yield* store.set({
|
|
25
|
+
...key,
|
|
26
|
+
value: {
|
|
27
|
+
...row,
|
|
28
|
+
props,
|
|
29
|
+
...output === void 0 ? {} : { attr: output }
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
const shouldAdopt = Effect.fn("Lifecycle.shouldAdopt")(function* (fqn) {
|
|
34
|
+
const stack = yield* Effect.serviceOption(Stack);
|
|
35
|
+
const resourcePolicy = Option.isSome(stack) ? stack.value.resources[fqn]?.Adopt : void 0;
|
|
36
|
+
if (resourcePolicy !== void 0) return resourcePolicy;
|
|
37
|
+
const policy = yield* Effect.serviceOption(AdoptPolicy);
|
|
38
|
+
if (Option.isSome(policy)) return policy.value;
|
|
39
|
+
const context = yield* Effect.serviceOption(AlchemyContext);
|
|
40
|
+
return Option.isSome(context) && context.value.adopt;
|
|
41
|
+
});
|
|
42
|
+
//#endregion
|
|
43
|
+
export { LifecycleStateError, checkpointCreate, shouldAdopt };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=Lifecycle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Lifecycle.js","names":[],"sources":["../../src/internal/Lifecycle.ts"],"sourcesContent":["import { AdoptPolicy } from \"alchemy/AdoptPolicy\";\nimport { AlchemyContext } from \"alchemy/AlchemyContext\";\nimport { Stack } from \"alchemy/Stack\";\nimport { State } from \"alchemy/State/State\";\nimport * as Effect from \"effect/Effect\";\nimport * as Option from \"effect/Option\";\nimport * as Schema from \"effect/Schema\";\n\nexport class LifecycleStateError extends Schema.TaggedError<LifecycleStateError>()(\n \"LifecycleStateError\",\n { fqn: Schema.String },\n) {}\n\nexport const checkpointCreate = Effect.fn(\"Lifecycle.checkpointCreate\")(\n function* (fqn: string, instanceId: string, props: object, output?: object) {\n const stack = yield* Effect.serviceOption(Stack);\n if (Option.isNone(stack)) return;\n const state = yield* Effect.serviceOption(State);\n if (Option.isNone(state)) return yield* new LifecycleStateError({ fqn });\n const store = yield* state.value;\n const key = { stack: stack.value.name, stage: stack.value.stage, fqn };\n const row = yield* store.get(key);\n if (!row || row.kind === \"action\" || row.instanceId !== instanceId)\n return yield* new LifecycleStateError({ fqn });\n if (row.status !== \"creating\") return;\n yield* store.set({\n ...key,\n value: {\n ...row,\n props,\n ...(output === undefined ? {} : { attr: output }),\n },\n });\n },\n);\n\nexport const shouldAdopt = Effect.fn(\"Lifecycle.shouldAdopt\")(function* (\n fqn: string,\n) {\n const stack = yield* Effect.serviceOption(Stack);\n const resourcePolicy = Option.isSome(stack)\n ? stack.value.resources[fqn]?.Adopt\n : undefined;\n if (resourcePolicy !== undefined) return resourcePolicy;\n const policy = yield* Effect.serviceOption(AdoptPolicy);\n if (Option.isSome(policy)) return policy.value;\n const context = yield* Effect.serviceOption(AlchemyContext);\n return Option.isSome(context) && context.value.adopt;\n});\n"],"mappings":";;;;;;;;AAQA,IAAa,sBAAb,cAAyC,OAAO,YAAiC,CAAC,CAChF,uBACA,EAAE,KAAK,OAAO,OAAO,CACvB,CAAC,CAAC,CAAC;AAEH,MAAa,mBAAmB,OAAO,GAAG,4BAA4B,CAAC,CACrE,WAAW,KAAa,YAAoB,OAAe,QAAiB;CAC1E,MAAM,QAAQ,OAAO,OAAO,cAAc,KAAK;CAC/C,IAAI,OAAO,OAAO,KAAK,GAAG;CAC1B,MAAM,QAAQ,OAAO,OAAO,cAAc,KAAK;CAC/C,IAAI,OAAO,OAAO,KAAK,GAAG,OAAO,OAAO,IAAI,oBAAoB,EAAE,IAAI,CAAC;CACvE,MAAM,QAAQ,OAAO,MAAM;CAC3B,MAAM,MAAM;EAAE,OAAO,MAAM,MAAM;EAAM,OAAO,MAAM,MAAM;EAAO;CAAI;CACrE,MAAM,MAAM,OAAO,MAAM,IAAI,GAAG;CAChC,IAAI,CAAC,OAAO,IAAI,SAAS,YAAY,IAAI,eAAe,YACtD,OAAO,OAAO,IAAI,oBAAoB,EAAE,IAAI,CAAC;CAC/C,IAAI,IAAI,WAAW,YAAY;CAC/B,OAAO,MAAM,IAAI;EACf,GAAG;EACH,OAAO;GACL,GAAG;GACH;GACA,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,OAAO;EACjD;CACF,CAAC;AACH,CACF;AAEA,MAAa,cAAc,OAAO,GAAG,uBAAuB,CAAC,CAAC,WAC5D,KACA;CACA,MAAM,QAAQ,OAAO,OAAO,cAAc,KAAK;CAC/C,MAAM,iBAAiB,OAAO,OAAO,KAAK,IACtC,MAAM,MAAM,UAAU,IAAI,EAAE,QAC5B,KAAA;CACJ,IAAI,mBAAmB,KAAA,GAAW,OAAO;CACzC,MAAM,SAAS,OAAO,OAAO,cAAc,WAAW;CACtD,IAAI,OAAO,OAAO,MAAM,GAAG,OAAO,OAAO;CACzC,MAAM,UAAU,OAAO,OAAO,cAAc,cAAc;CAC1D,OAAO,OAAO,OAAO,OAAO,KAAK,QAAQ,MAAM;AACjD,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Stack } from "alchemy/Stack";
|
|
2
|
+
import * as Crypto from "effect/Crypto";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as FileSystem from "effect/FileSystem";
|
|
5
|
+
import * as Path from "effect/Path";
|
|
6
|
+
import * as Redacted from "effect/Redacted";
|
|
7
|
+
import * as Schema from "effect/Schema";
|
|
8
|
+
import * as Scope from "effect/Scope";
|
|
9
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
10
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
11
|
+
/**
|
|
12
|
+
* The backend release a new local deployment runs. An existing data directory
|
|
13
|
+
* keeps running the release that created it, so upgrading this package never
|
|
14
|
+
* opens a database with a different release.
|
|
15
|
+
*/
|
|
16
|
+
export declare const backendVersion = "precompiled-2026-10-06-a3538c6";
|
|
17
|
+
declare const LocalBackendError_base: Schema.Class<LocalBackendError, Schema.TaggedStruct<"LocalBackendError", {
|
|
18
|
+
readonly deploymentName: Schema.String;
|
|
19
|
+
readonly reason: Schema.Literals<readonly ["configuration", "filesystem", "install", "identity", "ports", "running", "spawn", "exit", "timeout"]>;
|
|
20
|
+
readonly exitCode: Schema.optionalKey<Schema.Finite>;
|
|
21
|
+
}>, import("effect/Cause").YieldableError>;
|
|
22
|
+
export declare class LocalBackendError extends LocalBackendError_base {
|
|
23
|
+
get message(): string;
|
|
24
|
+
}
|
|
25
|
+
export interface LocalBackend {
|
|
26
|
+
readonly url: string;
|
|
27
|
+
readonly siteUrl: string;
|
|
28
|
+
readonly adminKey: Redacted.Redacted<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Completes when the backend process has exited and released its data
|
|
31
|
+
* directory, so the deployment can be booted again.
|
|
32
|
+
*/
|
|
33
|
+
readonly exited: Effect.Effect<void>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolves the directory that holds one local deployment's identity, database,
|
|
37
|
+
* and file storage: `.alchemy/convex/<stack>/<stage>/<deployment>`. Keyed by
|
|
38
|
+
* the deployment name because a deploy key resource only knows that name.
|
|
39
|
+
*/
|
|
40
|
+
export declare const dataDirectories: Effect.Effect<(deploymentName: string) => string, never, Path.Path | Stack>;
|
|
41
|
+
/**
|
|
42
|
+
* The admin key of a local deployment, for its deploy key resource.
|
|
43
|
+
*/
|
|
44
|
+
export declare const adminKey: (deploymentName: string, directory: string) => Effect.Effect<Redacted.Redacted<string>, LocalBackendError, FileSystem.FileSystem | Path.Path>;
|
|
45
|
+
export declare const remove: (deploymentName: string, directory: string) => Effect.Effect<void, LocalBackendError, FileSystem.FileSystem>;
|
|
46
|
+
/**
|
|
47
|
+
* Boot the local backend for one deployment and keep it running for the ambient
|
|
48
|
+
* scope. The process and the data directory's `owner.lock` share a child scope
|
|
49
|
+
* that closes when the boot fails or is interrupted and when the process exits,
|
|
50
|
+
* so a deployment can be booted again in the same session. The first boot
|
|
51
|
+
* installs the pinned release, generates an instance secret and admin key, and
|
|
52
|
+
* picks two free loopback ports; later boots reuse all of them, so stored
|
|
53
|
+
* credentials and URLs stay valid. A saved port that something else now holds
|
|
54
|
+
* is an error rather than a move, because resources keep the URL.
|
|
55
|
+
*/
|
|
56
|
+
export declare const boot: (deploymentName: string, directory: string) => Effect.Effect<{
|
|
57
|
+
url: string;
|
|
58
|
+
siteUrl: string;
|
|
59
|
+
adminKey: Redacted.Redacted<string>;
|
|
60
|
+
exited: Effect.Effect<void, never, never>;
|
|
61
|
+
}, LocalBackendError, ChildProcessSpawner.ChildProcessSpawner | Crypto.Crypto | FileSystem.FileSystem | HttpClient.HttpClient | Path.Path | Scope.Scope>;
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=LocalBackend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalBackend.d.ts","sourceRoot":"","sources":["../../src/internal/LocalBackend.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEtC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,KAAK,UAAU,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,IAAI,MAAM,aAAa,CAAC;AACpC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAE5C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AAGtC,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAgB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAOnE;;;;GAIG;AACH,eAAO,MAAM,cAAc,mCAAmC,CAAC;;;;;;AAE/D,qBAAa,iBAAkB,SAAQ,sBAiBtC;IACC,IAAa,OAAO,WAsBnB;CACF;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC7C;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;CACtC;AAmED;;;;GAIG;AACH,eAAO,MAAM,eAAe,iCASF,MAAM,sCAC9B,CAAC;AAuBH;;GAEG;AACH,eAAO,MAAM,QAAQ,+IAQnB,CAAC;AAEH,eAAO,MAAM,MAAM,8GAYjB,CAAC;AA0CH;;;;;;;;;GASG;AACH,eAAO,MAAM,IAAI;;;;;wJAiOf,CAAC"}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { processEnvironment } from "./CommandRunner.js";
|
|
2
|
+
import { origin, parsePortRange } from "./Loopback.js";
|
|
3
|
+
import { acquire } from "./FileLock.js";
|
|
4
|
+
import { userCache } from "./UserCache.js";
|
|
5
|
+
import { extract } from "./Zip.js";
|
|
6
|
+
import * as Effect from "effect/Effect";
|
|
7
|
+
import * as Redacted from "effect/Redacted";
|
|
8
|
+
import * as Schema from "effect/Schema";
|
|
9
|
+
import * as Config from "effect/Config";
|
|
10
|
+
import * as FileSystem from "effect/FileSystem";
|
|
11
|
+
import * as Path from "effect/Path";
|
|
12
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
13
|
+
import * as Option from "effect/Option";
|
|
14
|
+
import * as Crypto from "effect/Crypto";
|
|
15
|
+
import * as Scope from "effect/Scope";
|
|
16
|
+
import * as Schedule from "effect/Schedule";
|
|
17
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
18
|
+
import { dotAlchemyDirectory } from "alchemy/AlchemyContext";
|
|
19
|
+
import { Stack } from "alchemy/Stack";
|
|
20
|
+
import { createServer } from "node:net";
|
|
21
|
+
import * as Deferred from "effect/Deferred";
|
|
22
|
+
import * as Exit from "effect/Exit";
|
|
23
|
+
import * as Semaphore from "effect/Semaphore";
|
|
24
|
+
import * as Stream from "effect/Stream";
|
|
25
|
+
//#region src/internal/LocalBackend.ts
|
|
26
|
+
/**
|
|
27
|
+
* The backend release a new local deployment runs. An existing data directory
|
|
28
|
+
* keeps running the release that created it, so upgrading this package never
|
|
29
|
+
* opens a database with a different release.
|
|
30
|
+
*/
|
|
31
|
+
const backendVersion = "precompiled-2026-10-06-a3538c6";
|
|
32
|
+
var LocalBackendError = class extends Schema.TaggedError()("LocalBackendError", {
|
|
33
|
+
deploymentName: Schema.String,
|
|
34
|
+
reason: Schema.Literals([
|
|
35
|
+
"configuration",
|
|
36
|
+
"filesystem",
|
|
37
|
+
"install",
|
|
38
|
+
"identity",
|
|
39
|
+
"ports",
|
|
40
|
+
"running",
|
|
41
|
+
"spawn",
|
|
42
|
+
"exit",
|
|
43
|
+
"timeout"
|
|
44
|
+
]),
|
|
45
|
+
exitCode: Schema.optionalKey(Schema.Finite)
|
|
46
|
+
}) {
|
|
47
|
+
get message() {
|
|
48
|
+
const subject = `Local backend for ${this.deploymentName}`;
|
|
49
|
+
switch (this.reason) {
|
|
50
|
+
case "configuration": return `${subject} needs a home directory and, when set, CONFECT_LOCAL_BACKEND_PORTS as a range such as 3300-3399`;
|
|
51
|
+
case "filesystem": return `${subject} could not read or write its data directory`;
|
|
52
|
+
case "install": return `${subject} could not download or install the backend binary`;
|
|
53
|
+
case "identity": return `${subject} could not read, create, or save its identity`;
|
|
54
|
+
case "ports": return `${subject} could not get its ports; another process is using them`;
|
|
55
|
+
case "running": return `${subject} is already running: stop the other alchemy dev session, or the backend process left over from one, that serves its data directory`;
|
|
56
|
+
case "spawn": return `${subject} could not be started`;
|
|
57
|
+
case "exit": return `${subject} exited${this.exitCode === void 0 ? "" : ` with code ${this.exitCode}`} before it was ready`;
|
|
58
|
+
case "timeout": return `${subject} did not become ready in time`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const Identity = Schema.fromJsonString(Schema.Struct({
|
|
63
|
+
name: Schema.String,
|
|
64
|
+
version: Schema.String,
|
|
65
|
+
instanceSecret: Schema.String,
|
|
66
|
+
adminKey: Schema.String,
|
|
67
|
+
ports: Schema.optionalKey(Schema.Struct({
|
|
68
|
+
port: Schema.Int,
|
|
69
|
+
sitePort: Schema.Int
|
|
70
|
+
}))
|
|
71
|
+
}));
|
|
72
|
+
const artifacts = {
|
|
73
|
+
"darwin-arm64": "convex-local-backend-aarch64-apple-darwin.zip",
|
|
74
|
+
"darwin-x64": "convex-local-backend-x86_64-apple-darwin.zip",
|
|
75
|
+
"linux-arm64": "convex-local-backend-aarch64-unknown-linux-gnu.zip",
|
|
76
|
+
"linux-x64": "convex-local-backend-x86_64-unknown-linux-gnu.zip",
|
|
77
|
+
"win32-arm64": "convex-local-backend-x86_64-pc-windows-msvc.zip",
|
|
78
|
+
"win32-x64": "convex-local-backend-x86_64-pc-windows-msvc.zip"
|
|
79
|
+
};
|
|
80
|
+
const executable = process.platform === "win32" ? "convex-local-backend.exe" : "convex-local-backend";
|
|
81
|
+
const close = (server) => Effect.callback((resume) => {
|
|
82
|
+
server.close(() => resume(Effect.void));
|
|
83
|
+
});
|
|
84
|
+
const listen = (port) => Effect.acquireRelease(Effect.callback((resume) => {
|
|
85
|
+
const server = createServer();
|
|
86
|
+
server.once("error", () => resume(Effect.undefined));
|
|
87
|
+
server.listen(port, "127.0.0.1", () => resume(Effect.succeed(server)));
|
|
88
|
+
}), (server) => server ? close(server) : Effect.void);
|
|
89
|
+
const isFree = (port) => listen(port).pipe(Effect.map((server) => server !== void 0), Effect.scoped);
|
|
90
|
+
const portOf = (server) => {
|
|
91
|
+
const address = server?.address();
|
|
92
|
+
return typeof address === "object" && address !== null ? address.port : void 0;
|
|
93
|
+
};
|
|
94
|
+
const startup = Semaphore.makeUnsafe(1);
|
|
95
|
+
/**
|
|
96
|
+
* Resolves the directory that holds one local deployment's identity, database,
|
|
97
|
+
* and file storage: `.alchemy/convex/<stack>/<stage>/<deployment>`. Keyed by
|
|
98
|
+
* the deployment name because a deploy key resource only knows that name.
|
|
99
|
+
*/
|
|
100
|
+
const dataDirectories = Effect.gen(function* () {
|
|
101
|
+
const path = yield* Path.Path;
|
|
102
|
+
const stack = yield* Stack;
|
|
103
|
+
const root = path.resolve(yield* dotAlchemyDirectory, "convex", stack.name, stack.stage);
|
|
104
|
+
return (deploymentName) => path.join(root, deploymentName);
|
|
105
|
+
});
|
|
106
|
+
const readIdentity = Effect.fn("LocalBackend.readIdentity")(function* (deploymentName, directory) {
|
|
107
|
+
const fs = yield* FileSystem.FileSystem;
|
|
108
|
+
const file = (yield* Path.Path).join(directory, "identity.json");
|
|
109
|
+
const invalid = () => new LocalBackendError({
|
|
110
|
+
deploymentName,
|
|
111
|
+
reason: "identity"
|
|
112
|
+
});
|
|
113
|
+
if (!(yield* fs.exists(file).pipe(Effect.mapError(invalid)))) return Option.none();
|
|
114
|
+
const identity = yield* fs.readFileString(file).pipe(Effect.flatMap(Schema.decodeEffect(Identity)), Effect.mapError(invalid));
|
|
115
|
+
if (identity.name !== deploymentName) return yield* invalid();
|
|
116
|
+
return Option.some(identity);
|
|
117
|
+
});
|
|
118
|
+
/**
|
|
119
|
+
* The admin key of a local deployment, for its deploy key resource.
|
|
120
|
+
*/
|
|
121
|
+
const adminKey = Effect.fn("LocalBackend.adminKey")(function* (deploymentName, directory) {
|
|
122
|
+
const identity = yield* readIdentity(deploymentName, directory);
|
|
123
|
+
if (Option.isNone(identity)) return yield* new LocalBackendError({
|
|
124
|
+
deploymentName,
|
|
125
|
+
reason: "identity"
|
|
126
|
+
});
|
|
127
|
+
return Redacted.make(identity.value.adminKey);
|
|
128
|
+
});
|
|
129
|
+
const remove = Effect.fn("LocalBackend.remove")(function* (deploymentName, directory) {
|
|
130
|
+
yield* (yield* FileSystem.FileSystem).remove(directory, {
|
|
131
|
+
recursive: true,
|
|
132
|
+
force: true
|
|
133
|
+
}).pipe(Effect.mapError(() => new LocalBackendError({
|
|
134
|
+
deploymentName,
|
|
135
|
+
reason: "filesystem"
|
|
136
|
+
})));
|
|
137
|
+
});
|
|
138
|
+
/**
|
|
139
|
+
* Install a backend release into the Convex CLI's binary cache,
|
|
140
|
+
* `<cache>/convex/binaries/<version>/`, unless it is already there.
|
|
141
|
+
*/
|
|
142
|
+
const install = Effect.fn("LocalBackend.install")(function* (deploymentName, version, cache) {
|
|
143
|
+
const fs = yield* FileSystem.FileSystem;
|
|
144
|
+
const path = yield* Path.Path;
|
|
145
|
+
const http = yield* HttpClient.HttpClient;
|
|
146
|
+
const failed = () => new LocalBackendError({
|
|
147
|
+
deploymentName,
|
|
148
|
+
reason: "install"
|
|
149
|
+
});
|
|
150
|
+
const binaries = path.join(cache, "convex", "binaries");
|
|
151
|
+
const binary = path.join(binaries, version, executable);
|
|
152
|
+
const artifact = artifacts[`${process.platform}-${process.arch}`];
|
|
153
|
+
return yield* Effect.gen(function* () {
|
|
154
|
+
if (yield* fs.exists(binary)) return binary;
|
|
155
|
+
if (artifact === void 0) return yield* failed();
|
|
156
|
+
yield* fs.makeDirectory(binaries, { recursive: true });
|
|
157
|
+
const temp = yield* fs.makeTempDirectoryScoped({
|
|
158
|
+
directory: binaries,
|
|
159
|
+
prefix: ".download-"
|
|
160
|
+
});
|
|
161
|
+
const archive = path.join(temp, artifact);
|
|
162
|
+
const response = yield* http.get(`https://github.com/get-convex/convex-backend/releases/download/${version}/${artifact}`);
|
|
163
|
+
if (response.status !== 200) return yield* failed();
|
|
164
|
+
yield* response.stream.pipe(Stream.run(fs.sink(archive)));
|
|
165
|
+
const extracted = path.join(temp, executable);
|
|
166
|
+
yield* extract(archive, executable, extracted);
|
|
167
|
+
yield* fs.chmod(extracted, 493);
|
|
168
|
+
yield* fs.makeDirectory(path.dirname(binary), { recursive: true });
|
|
169
|
+
yield* fs.rename(extracted, binary);
|
|
170
|
+
return binary;
|
|
171
|
+
}).pipe(Effect.scoped, Effect.mapError(failed));
|
|
172
|
+
});
|
|
173
|
+
/**
|
|
174
|
+
* Boot the local backend for one deployment and keep it running for the ambient
|
|
175
|
+
* scope. The process and the data directory's `owner.lock` share a child scope
|
|
176
|
+
* that closes when the boot fails or is interrupted and when the process exits,
|
|
177
|
+
* so a deployment can be booted again in the same session. The first boot
|
|
178
|
+
* installs the pinned release, generates an instance secret and admin key, and
|
|
179
|
+
* picks two free loopback ports; later boots reuse all of them, so stored
|
|
180
|
+
* credentials and URLs stay valid. A saved port that something else now holds
|
|
181
|
+
* is an error rather than a move, because resources keep the URL.
|
|
182
|
+
*/
|
|
183
|
+
const boot = Effect.fn("LocalBackend.boot")(function* (deploymentName, directory) {
|
|
184
|
+
const path = yield* Path.Path;
|
|
185
|
+
const fs = yield* FileSystem.FileSystem;
|
|
186
|
+
const http = yield* HttpClient.HttpClient;
|
|
187
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
188
|
+
const crypto = yield* Crypto.Crypto;
|
|
189
|
+
const fail = (reason, exitCode) => new LocalBackendError({
|
|
190
|
+
deploymentName,
|
|
191
|
+
reason,
|
|
192
|
+
...exitCode === void 0 ? {} : { exitCode }
|
|
193
|
+
});
|
|
194
|
+
const settings = yield* Config.all({
|
|
195
|
+
environment: processEnvironment,
|
|
196
|
+
ports: Config.String("CONFECT_LOCAL_BACKEND_PORTS").pipe(Config.option)
|
|
197
|
+
}).pipe(Effect.mapError(() => fail("configuration")));
|
|
198
|
+
const cache = yield* userCache.pipe(Effect.mapError(() => fail("configuration")));
|
|
199
|
+
if (Option.isNone(cache)) return yield* fail("configuration");
|
|
200
|
+
const range = Option.map(settings.ports, parsePortRange);
|
|
201
|
+
if (Option.isSome(range) && Option.isNone(range.value)) return yield* fail("configuration");
|
|
202
|
+
const env = {
|
|
203
|
+
...settings.environment,
|
|
204
|
+
DISABLE_BEACON: "1",
|
|
205
|
+
DO_NOT_TRACK: "1"
|
|
206
|
+
};
|
|
207
|
+
const answer = (url) => http.get(`${url}/instance_name`).pipe(Effect.flatMap((response) => response.status === 200 ? response.text : Effect.undefined), Effect.orElseSucceed(() => void 0));
|
|
208
|
+
const allocate = Effect.gen(function* () {
|
|
209
|
+
if (Option.isNone(range) || Option.isNone(range.value)) {
|
|
210
|
+
const [port, sitePort] = yield* Effect.all([listen(0), listen(0)]).pipe(Effect.map((servers) => servers.map(portOf)), Effect.scoped);
|
|
211
|
+
if (port === void 0 || sitePort === void 0) return yield* fail("ports");
|
|
212
|
+
return {
|
|
213
|
+
port,
|
|
214
|
+
sitePort
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const free = [];
|
|
218
|
+
const { from, to } = range.value.value;
|
|
219
|
+
for (let port = from; port <= to && free.length < 2; port++) if (yield* isFree(port)) free.push(port);
|
|
220
|
+
const [port, sitePort] = free;
|
|
221
|
+
if (port === void 0 || sitePort === void 0) return yield* fail("ports");
|
|
222
|
+
return {
|
|
223
|
+
port,
|
|
224
|
+
sitePort
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
const generate = (binary) => Effect.gen(function* () {
|
|
228
|
+
const instanceSecret = Buffer.from(yield* crypto.randomBytes(32)).toString("hex");
|
|
229
|
+
const key = (yield* spawner.string(ChildProcess.make(binary, [
|
|
230
|
+
"keygen",
|
|
231
|
+
"admin-key",
|
|
232
|
+
"--instance-name",
|
|
233
|
+
deploymentName,
|
|
234
|
+
"--instance-secret",
|
|
235
|
+
instanceSecret
|
|
236
|
+
], {
|
|
237
|
+
env,
|
|
238
|
+
extendEnv: false,
|
|
239
|
+
stdin: "ignore",
|
|
240
|
+
stderr: "ignore"
|
|
241
|
+
}))).trim();
|
|
242
|
+
if (!key.startsWith(`${deploymentName}|`)) return yield* fail("identity");
|
|
243
|
+
return {
|
|
244
|
+
instanceSecret,
|
|
245
|
+
adminKey: key
|
|
246
|
+
};
|
|
247
|
+
}).pipe(Effect.catchTag("PlatformError", () => Effect.fail(fail("identity"))));
|
|
248
|
+
const save = (identity) => Effect.gen(function* () {
|
|
249
|
+
const file = path.join(directory, "identity.json");
|
|
250
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
251
|
+
yield* fs.writeFileString(temp, yield* Schema.encodeEffect(Identity)(identity), { mode: 384 });
|
|
252
|
+
yield* fs.rename(temp, file);
|
|
253
|
+
}).pipe(Effect.uninterruptible, Effect.mapError(() => fail("identity")));
|
|
254
|
+
const session = yield* Scope.Scope;
|
|
255
|
+
const owned = yield* Scope.fork(session);
|
|
256
|
+
const stopped = yield* Deferred.make();
|
|
257
|
+
yield* Scope.addFinalizer(owned, Deferred.succeed(stopped, void 0));
|
|
258
|
+
const start = Effect.gen(function* () {
|
|
259
|
+
yield* acquire(path.join(directory, "owner.lock")).pipe(Effect.mapError((error) => fail(error.reason === "held" ? "running" : "filesystem")));
|
|
260
|
+
const saved = yield* readIdentity(deploymentName, directory);
|
|
261
|
+
const version = Option.isSome(saved) ? saved.value.version : backendVersion;
|
|
262
|
+
const binary = yield* install(deploymentName, version, cache.value);
|
|
263
|
+
let identity;
|
|
264
|
+
if (Option.isSome(saved)) identity = saved.value;
|
|
265
|
+
else {
|
|
266
|
+
identity = {
|
|
267
|
+
name: deploymentName,
|
|
268
|
+
version,
|
|
269
|
+
...yield* generate(binary)
|
|
270
|
+
};
|
|
271
|
+
yield* save(identity);
|
|
272
|
+
}
|
|
273
|
+
const ports = identity.ports ?? (yield* allocate);
|
|
274
|
+
if (identity.ports !== void 0) {
|
|
275
|
+
if (!(yield* isFree(ports.port)) || !(yield* isFree(ports.sitePort))) return yield* fail((yield* answer(origin(ports.port))) === deploymentName ? "running" : "ports");
|
|
276
|
+
}
|
|
277
|
+
const url = origin(ports.port);
|
|
278
|
+
const siteUrl = origin(ports.sitePort);
|
|
279
|
+
const handle = yield* spawner.spawn(ChildProcess.make(binary, [
|
|
280
|
+
"--interface",
|
|
281
|
+
"127.0.0.1",
|
|
282
|
+
"--port",
|
|
283
|
+
String(ports.port),
|
|
284
|
+
"--site-proxy-port",
|
|
285
|
+
String(ports.sitePort),
|
|
286
|
+
"--instance-name",
|
|
287
|
+
deploymentName,
|
|
288
|
+
"--instance-secret",
|
|
289
|
+
identity.instanceSecret,
|
|
290
|
+
"--convex-origin",
|
|
291
|
+
url,
|
|
292
|
+
"--convex-site",
|
|
293
|
+
siteUrl,
|
|
294
|
+
"--local-storage",
|
|
295
|
+
path.join(directory, "convex_local_storage"),
|
|
296
|
+
"--disable-beacon",
|
|
297
|
+
path.join(directory, "convex_local_backend.sqlite3")
|
|
298
|
+
], {
|
|
299
|
+
cwd: directory,
|
|
300
|
+
env,
|
|
301
|
+
extendEnv: false,
|
|
302
|
+
stdin: "ignore",
|
|
303
|
+
stdout: "ignore",
|
|
304
|
+
stderr: "ignore",
|
|
305
|
+
forceKillAfter: "3 seconds"
|
|
306
|
+
})).pipe(Effect.mapError(() => fail("spawn")));
|
|
307
|
+
const ready = answer(url).pipe(Effect.flatMap((name) => name === void 0 || name === deploymentName ? Effect.succeed(name === deploymentName) : Effect.fail(fail("ports"))), Effect.repeat({
|
|
308
|
+
until: (isReady) => isReady,
|
|
309
|
+
schedule: Schedule.spaced("100 millis")
|
|
310
|
+
}));
|
|
311
|
+
const exit = handle.exitCode.pipe(Effect.mapError(() => fail("exit")), Effect.flatMap((code) => Effect.fail(fail("exit", Number(code)))));
|
|
312
|
+
yield* Effect.raceFirst(ready, exit).pipe(Effect.timeoutOrElse({
|
|
313
|
+
duration: "60 seconds",
|
|
314
|
+
orElse: () => Effect.fail(fail("timeout"))
|
|
315
|
+
}));
|
|
316
|
+
if (identity.ports === void 0) yield* save({
|
|
317
|
+
...identity,
|
|
318
|
+
ports
|
|
319
|
+
});
|
|
320
|
+
yield* handle.exitCode.pipe(Effect.ignore, Effect.andThen(Scope.close(owned, Exit.void)), Effect.forkIn(session));
|
|
321
|
+
return {
|
|
322
|
+
url,
|
|
323
|
+
siteUrl,
|
|
324
|
+
adminKey: Redacted.make(identity.adminKey),
|
|
325
|
+
exited: Deferred.await(stopped)
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
return yield* startup.withPermits(1)(start).pipe(Scope.provide(owned), Effect.onExit((exit) => Exit.isSuccess(exit) ? Effect.void : Scope.close(owned, exit)));
|
|
329
|
+
});
|
|
330
|
+
//#endregion
|
|
331
|
+
export { LocalBackendError, adminKey, backendVersion, boot, dataDirectories, remove };
|
|
332
|
+
|
|
333
|
+
//# sourceMappingURL=LocalBackend.js.map
|