@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.
Files changed (113) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +22 -2
  3. package/dist/Backend.d.ts +45 -0
  4. package/dist/Backend.d.ts.map +1 -0
  5. package/dist/Backend.js +38 -0
  6. package/dist/Backend.js.map +1 -0
  7. package/dist/Code.d.ts +80 -0
  8. package/dist/Code.d.ts.map +1 -0
  9. package/dist/Code.js +29 -0
  10. package/dist/Code.js.map +1 -0
  11. package/dist/Confect.d.ts +2 -0
  12. package/dist/Confect.d.ts.map +1 -0
  13. package/dist/Confect.js +8 -0
  14. package/dist/Confect.js.map +1 -0
  15. package/dist/Convex.d.ts +7 -0
  16. package/dist/Convex.d.ts.map +1 -0
  17. package/dist/Convex.js +21 -0
  18. package/dist/Convex.js.map +1 -0
  19. package/dist/ConvexClient.d.ts +215 -0
  20. package/dist/ConvexClient.d.ts.map +1 -0
  21. package/dist/ConvexClient.js +224 -0
  22. package/dist/ConvexClient.js.map +1 -0
  23. package/dist/DeployKey.d.ts +120 -0
  24. package/dist/DeployKey.d.ts.map +1 -0
  25. package/dist/DeployKey.js +151 -0
  26. package/dist/DeployKey.js.map +1 -0
  27. package/dist/Deployment.d.ts +113 -0
  28. package/dist/Deployment.d.ts.map +1 -0
  29. package/dist/Deployment.js +160 -0
  30. package/dist/Deployment.js.map +1 -0
  31. package/dist/EnvironmentVariables.d.ts +155 -0
  32. package/dist/EnvironmentVariables.d.ts.map +1 -0
  33. package/dist/EnvironmentVariables.js +135 -0
  34. package/dist/EnvironmentVariables.js.map +1 -0
  35. package/dist/Project.d.ts +100 -0
  36. package/dist/Project.d.ts.map +1 -0
  37. package/dist/Project.js +123 -0
  38. package/dist/Project.js.map +1 -0
  39. package/dist/Providers.d.ts +17 -0
  40. package/dist/Providers.d.ts.map +1 -0
  41. package/dist/Providers.js +46 -0
  42. package/dist/Providers.js.map +1 -0
  43. package/dist/_virtual/_rolldown/runtime.js +13 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +3 -0
  47. package/dist/internal/CodeDeployment.d.ts +17 -0
  48. package/dist/internal/CodeDeployment.d.ts.map +1 -0
  49. package/dist/internal/CodeDeployment.js +53 -0
  50. package/dist/internal/CodeDeployment.js.map +1 -0
  51. package/dist/internal/CommandRunner.d.ts +55 -0
  52. package/dist/internal/CommandRunner.d.ts.map +1 -0
  53. package/dist/internal/CommandRunner.js +126 -0
  54. package/dist/internal/CommandRunner.js.map +1 -0
  55. package/dist/internal/FileLock.d.ts +30 -0
  56. package/dist/internal/FileLock.d.ts.map +1 -0
  57. package/dist/internal/FileLock.js +83 -0
  58. package/dist/internal/FileLock.js.map +1 -0
  59. package/dist/internal/Lifecycle.d.ts +11 -0
  60. package/dist/internal/Lifecycle.d.ts.map +1 -0
  61. package/dist/internal/Lifecycle.js +45 -0
  62. package/dist/internal/Lifecycle.js.map +1 -0
  63. package/dist/internal/LocalBackend.d.ts +63 -0
  64. package/dist/internal/LocalBackend.d.ts.map +1 -0
  65. package/dist/internal/LocalBackend.js +333 -0
  66. package/dist/internal/LocalBackend.js.map +1 -0
  67. package/dist/internal/LocalSidecar.d.ts +3 -0
  68. package/dist/internal/LocalSidecar.d.ts.map +1 -0
  69. package/dist/internal/LocalSidecar.js +7 -0
  70. package/dist/internal/LocalSidecar.js.map +1 -0
  71. package/dist/internal/Loopback.d.ts +18 -0
  72. package/dist/internal/Loopback.d.ts.map +1 -0
  73. package/dist/internal/Loopback.js +31 -0
  74. package/dist/internal/Loopback.js.map +1 -0
  75. package/dist/internal/PushLock.d.ts +36 -0
  76. package/dist/internal/PushLock.d.ts.map +1 -0
  77. package/dist/internal/PushLock.js +61 -0
  78. package/dist/internal/PushLock.js.map +1 -0
  79. package/dist/internal/ResourceIdentity.d.ts +3 -0
  80. package/dist/internal/ResourceIdentity.d.ts.map +1 -0
  81. package/dist/internal/ResourceIdentity.js +10 -0
  82. package/dist/internal/ResourceIdentity.js.map +1 -0
  83. package/dist/internal/UserCache.d.ts +12 -0
  84. package/dist/internal/UserCache.d.ts.map +1 -0
  85. package/dist/internal/UserCache.js +25 -0
  86. package/dist/internal/UserCache.js.map +1 -0
  87. package/dist/internal/Zip.d.ts +15 -0
  88. package/dist/internal/Zip.d.ts.map +1 -0
  89. package/dist/internal/Zip.js +65 -0
  90. package/dist/internal/Zip.js.map +1 -0
  91. package/package.json +65 -3
  92. package/src/Backend.ts +101 -0
  93. package/src/Code.ts +55 -0
  94. package/src/Confect.ts +1 -0
  95. package/src/Convex.ts +18 -0
  96. package/src/ConvexClient.ts +544 -0
  97. package/src/DeployKey.ts +256 -0
  98. package/src/Deployment.ts +277 -0
  99. package/src/EnvironmentVariables.ts +254 -0
  100. package/src/Project.ts +205 -0
  101. package/src/Providers.ts +67 -0
  102. package/src/index.ts +2 -0
  103. package/src/internal/CodeDeployment.ts +81 -0
  104. package/src/internal/CommandRunner.ts +251 -0
  105. package/src/internal/FileLock.ts +136 -0
  106. package/src/internal/Lifecycle.ts +49 -0
  107. package/src/internal/LocalBackend.ts +492 -0
  108. package/src/internal/LocalSidecar.ts +9 -0
  109. package/src/internal/Loopback.ts +34 -0
  110. package/src/internal/PushLock.ts +99 -0
  111. package/src/internal/ResourceIdentity.ts +20 -0
  112. package/src/internal/UserCache.ts +25 -0
  113. 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