@rebasepro/cli 0.16.0 → 0.16.1-canary.g041c925
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/dist/bundle.d.ts +28 -2
- package/dist/commands/build.d.ts +10 -0
- package/dist/commands/cloud/deployments.d.ts +41 -0
- package/dist/commands/cloud/projects.d.ts +14 -4
- package/dist/commands/cloud/resources.d.ts +10 -1
- package/dist/commands/db.d.ts +16 -0
- package/dist/commands/dev.d.ts +11 -0
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/resources.d.ts +1 -0
- package/dist/constraints-BK1_4vci.js +80 -0
- package/dist/constraints-BK1_4vci.js.map +1 -0
- package/dist/daemon-Bdl4lrdt.js +252 -0
- package/dist/daemon-Bdl4lrdt.js.map +1 -0
- package/dist/daemon-entry-Brq-S8XX.js +378 -0
- package/dist/daemon-entry-Brq-S8XX.js.map +1 -0
- package/dist/dev-db/__fixtures__/cli-entry.d.ts +1 -0
- package/dist/dev-db/constraints.d.ts +98 -0
- package/dist/dev-db/daemon-entry.d.ts +35 -0
- package/dist/dev-db/daemon.d.ts +92 -0
- package/dist/dev-db/notification-proxy.d.ts +102 -0
- package/dist/dev-db/prepare.d.ts +63 -0
- package/dist/dev-db/pull.d.ts +92 -0
- package/dist/dev-db/resolve.d.ts +66 -0
- package/dist/dev-db/state.d.ts +93 -0
- package/dist/function-portability.d.ts +45 -0
- package/dist/index.d.ts +17 -17
- package/dist/index.es.js +5699 -4122
- package/dist/index.es.js.map +1 -1
- package/dist/manifest.d.ts +24 -1
- package/dist/pull-DqPRu1te.js +167 -0
- package/dist/pull-DqPRu1te.js.map +1 -0
- package/dist/resources/derive.d.ts +47 -0
- package/dist/resources/eject-infra-command.d.ts +7 -0
- package/dist/resources/eject-infra.d.ts +46 -0
- package/dist/state-c0CJ6Kwb.js +190 -0
- package/dist/state-c0CJ6Kwb.js.map +1 -0
- package/dist/telemetry/consent.d.ts +1 -1
- package/dist/telemetry/index.d.ts +7 -7
- package/dist/utils/dev-preflight.d.ts +73 -0
- package/package.json +13 -8
- package/templates/eject/backend/src/index.ts +15 -8
- package/templates/eject/config/resources.ts +24 -0
- package/templates/template/AGENTS.md +1 -1
- package/templates/template/CLAUDE.md +1 -1
- package/templates/template/README.md +1 -1
- package/templates/template/ai-instructions.md +5 -2
- package/templates/template/backend/functions/hello.ts +43 -22
- package/templates/template/docker-compose.yml +10 -1
- package/templates/template/gitignore +1 -0
package/dist/bundle.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ResourceGraph, type NativeDependency, type RebaseBundleManifest, type RebaseBackendAppConfig } from "@rebasepro/types";
|
|
2
2
|
export declare const DEFAULT_BUNDLE_DIR = "dist-bundle";
|
|
3
3
|
export interface BuildBundleOptions {
|
|
4
4
|
projectRoot: string;
|
|
@@ -14,7 +14,14 @@ export interface BuildBundleOptions {
|
|
|
14
14
|
* Passed in rather than re-read here so `rebase.json` is parsed and validated
|
|
15
15
|
* once, by the command that owns it.
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The project's resource graph, derived from its config.
|
|
19
|
+
*
|
|
20
|
+
* Recorded in the bundle manifest so a host can read what the project needs
|
|
21
|
+
* without running it — which is what lets a console show "wants a `media`
|
|
22
|
+
* bucket, has none" before a first deploy has produced anything.
|
|
23
|
+
*/
|
|
24
|
+
resources?: ResourceGraph;
|
|
18
25
|
/**
|
|
19
26
|
* Install the declared dependencies into the bundle at build time.
|
|
20
27
|
*
|
|
@@ -193,6 +200,25 @@ export interface VendorResult {
|
|
|
193
200
|
* the upload is compressed: crossing it means "getting close", not "will fail".
|
|
194
201
|
*/
|
|
195
202
|
export declare const VENDOR_SIZE_WARN_BYTES: number;
|
|
203
|
+
/**
|
|
204
|
+
* Where vendoring stops being an optimisation and becomes a bundle nobody can
|
|
205
|
+
* deploy.
|
|
206
|
+
*
|
|
207
|
+
* Past this, shipping the tree anyway trades a faster cold start for a 413 — and
|
|
208
|
+
* the 413 arrives at deploy time, after a build nobody watches, with a remedy
|
|
209
|
+
* (`--no-vendor`) that requires knowing this happened. Unvendoring here costs
|
|
210
|
+
* 40–60s of cold start and produces a bundle that uploads; that is the better
|
|
211
|
+
* side of the trade to be on by default.
|
|
212
|
+
*
|
|
213
|
+
* 200 MB assumes a **2x** floor on compression, which is pessimistic for a tree
|
|
214
|
+
* of JavaScript (3–5x is typical) and deliberately so: source maps and prebuilt
|
|
215
|
+
* binaries compress far worse than source, and the failure this prevents is
|
|
216
|
+
* asymmetric — a bundle refused at the door versus a minute of cold start.
|
|
217
|
+
* `--vendor` overrides it, for a deploy path with no upload at all (a Dockerfile
|
|
218
|
+
* built from source, where the tree is copied into an image and the control
|
|
219
|
+
* plane never sees it).
|
|
220
|
+
*/
|
|
221
|
+
export declare const VENDOR_SIZE_MAX_BYTES: number;
|
|
196
222
|
/**
|
|
197
223
|
* Install the bundle's declared dependencies into the bundle itself.
|
|
198
224
|
*
|
package/dist/commands/build.d.ts
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
|
+
import type { RebaseAppConfig } from "@rebasepro/types";
|
|
1
2
|
export declare function buildCommand(rawArgs?: string[]): Promise<void>;
|
|
3
|
+
/**
|
|
4
|
+
* Build a static app and package it into a static bundle.
|
|
5
|
+
*
|
|
6
|
+
* Runs the app's own build command, checks it produced the declared output, then
|
|
7
|
+
* packages that output into a `static`-kind bundle — the same deployable shape as
|
|
8
|
+
* a backend bundle, so a frontend or admin app deploys through the identical
|
|
9
|
+
* path and runs on the identical image, just serving files instead of an API.
|
|
10
|
+
*/
|
|
11
|
+
export declare function buildAssetApp(projectRoot: string, name: string, app: RebaseAppConfig, runtimeRange: string, outOverride?: string): Promise<string | undefined>;
|
|
@@ -37,6 +37,47 @@ export declare function triggerInfo(dep: DeploymentRow): {
|
|
|
37
37
|
};
|
|
38
38
|
/** Shape one deployment row into the stable JSON view the CLI publishes. */
|
|
39
39
|
export declare function deploymentView(dep: DeploymentRow): Record<string, unknown>;
|
|
40
|
+
/** What `POST /api/functions/deploy/cancel` answers. */
|
|
41
|
+
export interface CancelResponse {
|
|
42
|
+
success: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The deployment that was cancelled — `null` when there was none.
|
|
45
|
+
*
|
|
46
|
+
* Nullable because "nothing was in flight" is a real, successful outcome
|
|
47
|
+
* and not an error: a project's `status` column can claim `deploying` while
|
|
48
|
+
* no deployment row backs the claim, and cancelling is what clears it.
|
|
49
|
+
*/
|
|
50
|
+
deploymentId: string | null;
|
|
51
|
+
buildJobDeleted: boolean;
|
|
52
|
+
/** The request cleared a stuck `deploying` claim rather than stopping a build. */
|
|
53
|
+
unstranded?: boolean;
|
|
54
|
+
/** The status the project settled on when it did. */
|
|
55
|
+
projectStatus?: string | null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* What `rebase cloud cancel` says it did.
|
|
59
|
+
*
|
|
60
|
+
* Split out of the command because there are now two outcomes behind one
|
|
61
|
+
* `success: true`, and only one of them is "a build was stopped". The control
|
|
62
|
+
* plane learned to clear a project stranded at `deploying` with no deployment
|
|
63
|
+
* row behind it — the state `prospector` was wedged in for five days — and it
|
|
64
|
+
* reports that through this same endpoint. An unbranched caller printed
|
|
65
|
+
* "Cancelled deployment null" for it, which reads as a bug in the thing that
|
|
66
|
+
* had just fixed the bug.
|
|
67
|
+
*
|
|
68
|
+
* The ABSENT ID is what decides, not the `unstranded` flag: the flag is the
|
|
69
|
+
* server being explicit, and it is the newer half of the contract. Branching on
|
|
70
|
+
* the id means a client that meets a control plane which grew the behaviour
|
|
71
|
+
* without the flag still says something true.
|
|
72
|
+
*/
|
|
73
|
+
export declare function cancelView(res: CancelResponse): {
|
|
74
|
+
/** The headline, for `success()`. */
|
|
75
|
+
headline: string;
|
|
76
|
+
/** Dimmed follow-ups, printed under it. */
|
|
77
|
+
notes: string[];
|
|
78
|
+
/** The stable JSON view, which is what a piped run gets. */
|
|
79
|
+
json: Record<string, unknown>;
|
|
80
|
+
};
|
|
40
81
|
/**
|
|
41
82
|
* Rows shown when `--limit` is not given.
|
|
42
83
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DeployTarget, type CloudClient } from "./context";
|
|
1
|
+
import { type DeployTarget, type CloudClient } from "./context.js";
|
|
2
2
|
export declare function listProjects(rawArgs: string[]): Promise<void>;
|
|
3
3
|
/**
|
|
4
4
|
* Where this project says it runs.
|
|
@@ -9,8 +9,9 @@ export declare function listProjects(rawArgs: string[]): Promise<void>;
|
|
|
9
9
|
* never contradicted by a failure; it just sits in the record. The CLI used to
|
|
10
10
|
* default to `hetzner`/`nbg1` unconditionally, which is how projects running on
|
|
11
11
|
* our GKE cluster came to describe themselves as Hetzner in the console — and
|
|
12
|
-
* `provider`
|
|
13
|
-
*
|
|
12
|
+
* `provider` also decides which substrate's rules a project's dials are clamped
|
|
13
|
+
* to — Autopilot's 250m floor and 1:1-6.5:1 band do not apply on Hetzner or EKS
|
|
14
|
+
* — so a wrong value here resizes pods, it is not a cosmetic slip.
|
|
14
15
|
*
|
|
15
16
|
* The control plane already publishes the infrastructure that actually exists,
|
|
16
17
|
* and the console's create wizard reads it. Ask the same question here.
|
|
@@ -39,10 +40,19 @@ export declare const CREATE_PROJECT_FLAGS: {
|
|
|
39
40
|
readonly "--branch": StringConstructor;
|
|
40
41
|
readonly "--provider": StringConstructor;
|
|
41
42
|
readonly "--region": StringConstructor;
|
|
42
|
-
readonly "--vm-size": StringConstructor;
|
|
43
43
|
readonly "--org": StringConstructor;
|
|
44
44
|
readonly "--link": BooleanConstructor;
|
|
45
45
|
readonly "-n": "--name";
|
|
46
|
+
readonly "--cpu": StringConstructor;
|
|
47
|
+
readonly "--memory": StringConstructor;
|
|
48
|
+
readonly "--replicas": StringConstructor;
|
|
49
|
+
readonly "--spot": StringConstructor;
|
|
50
|
+
readonly "--scale-to-zero": StringConstructor;
|
|
51
|
+
readonly "--db-mode": StringConstructor;
|
|
52
|
+
readonly "--db-instances": StringConstructor;
|
|
53
|
+
readonly "--db-cpu": StringConstructor;
|
|
54
|
+
readonly "--db-memory": StringConstructor;
|
|
55
|
+
readonly "--storage": StringConstructor;
|
|
46
56
|
};
|
|
47
57
|
export declare function createProject(rawArgs: string[]): Promise<void>;
|
|
48
58
|
/**
|
|
@@ -106,7 +106,16 @@ export declare function resourcesCommand(action: string | undefined, rawArgs: st
|
|
|
106
106
|
* the same shape `buildSettingsPatch` uses. Returns an error string rather than
|
|
107
107
|
* throwing, because the caller owns how a refusal is printed in JSON mode.
|
|
108
108
|
*/
|
|
109
|
-
export declare function buildDialPatch(rawArgs: string[]
|
|
109
|
+
export declare function buildDialPatch(rawArgs: string[],
|
|
110
|
+
/**
|
|
111
|
+
* `requireOne: false` for `projects create`, where naming no dial is the
|
|
112
|
+
* ordinary case — a new project takes the platform default. On
|
|
113
|
+
* `resources set` a patch with nothing in it is a typo, and saying so beats
|
|
114
|
+
* a success message for a change nobody made.
|
|
115
|
+
*/
|
|
116
|
+
opts?: {
|
|
117
|
+
requireOne?: boolean;
|
|
118
|
+
}): {
|
|
110
119
|
patch: Record<string, unknown>;
|
|
111
120
|
error?: string;
|
|
112
121
|
};
|
package/dist/commands/db.d.ts
CHANGED
|
@@ -13,4 +13,20 @@
|
|
|
13
13
|
* is actually changed, and leaves the plugin usable on its own terms.
|
|
14
14
|
*/
|
|
15
15
|
export declare function absolutizeLocalPathArgs(args: string[], cwd: string): string[];
|
|
16
|
+
/**
|
|
17
|
+
* Run a database subcommand through the active driver's CLI, throwing on
|
|
18
|
+
* failure instead of exiting.
|
|
19
|
+
*
|
|
20
|
+
* `dbCommand` below turns every failure into `process.exit(1)`, which is right
|
|
21
|
+
* for a command the user invoked directly and wrong for a caller that has more
|
|
22
|
+
* to do afterwards — `rebase dev` runs a schema push during start-up and must
|
|
23
|
+
* survive it failing. Exiting is therefore the wrapper's job, not this
|
|
24
|
+
* function's.
|
|
25
|
+
*
|
|
26
|
+
* The database is resolved *here* rather than in the wrapper, so the schema
|
|
27
|
+
* push `rebase dev` performs during start-up reaches the managed database too.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runDriverDbCommand(rawArgs: string[], options?: {
|
|
30
|
+
quiet?: boolean;
|
|
31
|
+
}): Promise<void>;
|
|
16
32
|
export declare function dbCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void>;
|
package/dist/commands/dev.d.ts
CHANGED
|
@@ -20,9 +20,20 @@ export declare const DEV_FLAGS: {
|
|
|
20
20
|
readonly "--frontend-only": BooleanConstructor;
|
|
21
21
|
readonly "--port": NumberConstructor;
|
|
22
22
|
readonly "--generate": BooleanConstructor;
|
|
23
|
+
/**
|
|
24
|
+
* Point this run at a database of your own, ahead of everything else.
|
|
25
|
+
*
|
|
26
|
+
* The managed development database fills a vacuum; it never redirects a
|
|
27
|
+
* project that has said which Postgres it wants. This flag is the loudest
|
|
28
|
+
* way to say it, and outranks DATABASE_URL in the environment and in .env.
|
|
29
|
+
*/
|
|
30
|
+
readonly "--database-url": StringConstructor;
|
|
31
|
+
/** Use Postgres in Docker rather than the managed database. */
|
|
32
|
+
readonly "--docker": BooleanConstructor;
|
|
23
33
|
readonly "-b": "--backend-only";
|
|
24
34
|
readonly "-f": "--frontend-only";
|
|
25
35
|
readonly "-P": "--port";
|
|
26
36
|
readonly "-g": "--generate";
|
|
37
|
+
readonly "--no-db": BooleanConstructor;
|
|
27
38
|
};
|
|
28
39
|
export declare function devCommand(rawArgs: string[]): Promise<void>;
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resourcesCommand(rawArgs: string[]): Promise<void>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
//#region src/dev-db/constraints.ts
|
|
2
|
+
/**
|
|
3
|
+
* What PGlite can and cannot do as a development database, measured rather
|
|
4
|
+
* than assumed.
|
|
5
|
+
*
|
|
6
|
+
* Everything in this directory is shaped by four facts, each established by
|
|
7
|
+
* running it against `@electric-sql/pglite` 0.5.6 and
|
|
8
|
+
* `@electric-sql/pglite-socket` 0.2.9 rather than by reading their docs. They
|
|
9
|
+
* are recorded here because two of them are silent failures — the kind that
|
|
10
|
+
* make a developer lose an evening to a feature that reports success and does
|
|
11
|
+
* nothing.
|
|
12
|
+
*
|
|
13
|
+
* 1. **It is really PostgreSQL 18.3.** `select version()` over the socket
|
|
14
|
+
* returns `PostgreSQL 18.3 (PGlite 0.5.6) on wasm32`, which is the same
|
|
15
|
+
* major as the `postgres:18-alpine` the eject template ships. So a dev
|
|
16
|
+
* database here and a compose database there are the same Postgres, and
|
|
17
|
+
* schema behaviour does not diverge between them.
|
|
18
|
+
*
|
|
19
|
+
* 2. **`pg_trgm` and `unaccent` are available**, which is what search
|
|
20
|
+
* collections need. They are not installed by a bare `CREATE EXTENSION`,
|
|
21
|
+
* though — PGlite ships them as separate bundles that must be passed to the
|
|
22
|
+
* constructor, and without that `CREATE EXTENSION pg_trgm` fails with
|
|
23
|
+
* `extension "pg_trgm" is not available`. {@link PGLITE_EXTENSIONS} is that
|
|
24
|
+
* list, and it has to stay in step with what the schema generator emits.
|
|
25
|
+
*
|
|
26
|
+
* 3. **RLS is enforced exactly as it is on a real server.** With
|
|
27
|
+
* `SET LOCAL ROLE "rebase_user"` inside a transaction — which is how
|
|
28
|
+
* `PostgresBackendDriver` isolates every request — `current_user` becomes
|
|
29
|
+
* the restricted role, `session_user` stays the owner, and a policy using
|
|
30
|
+
* `current_setting('app.tenant')` filters rows correctly, including under
|
|
31
|
+
* `FORCE ROW LEVEL SECURITY`. Measured: an owner saw 3 rows and the
|
|
32
|
+
* role-switched transaction saw 2, with the cross-tenant probe returning 0.
|
|
33
|
+
* This is the one that mattered most: a dev database that quietly failed to
|
|
34
|
+
* apply RLS would give false confidence about the product's central claim.
|
|
35
|
+
*
|
|
36
|
+
* 4. **Concurrency is the real limit, and it fails badly.** PGlite is a single
|
|
37
|
+
* session, and `PGLiteSocketServer` multiplexes connections onto it. Two
|
|
38
|
+
* pooled clients that hold *overlapping transactions* deadlock — not error,
|
|
39
|
+
* hang — which is precisely what a request-per-transaction server does under
|
|
40
|
+
* any concurrent load. {@link MANAGED_POOL_MAX} is the answer: one client
|
|
41
|
+
* connection, so requests queue in the pool instead of deadlocking in the
|
|
42
|
+
* multiplexer. Measured: with a pool of 1, four concurrent queries and a
|
|
43
|
+
* role-switched RLS transaction all pass; with a pool of 5 the same script
|
|
44
|
+
* hangs indefinitely.
|
|
45
|
+
*
|
|
46
|
+
* 5. **LISTEN/NOTIFY needed repairing, and now works.** A notification is an
|
|
47
|
+
* asynchronous message with no request to answer, and the multiplexer hands
|
|
48
|
+
* it to whichever socket is reading rather than to the one that issued
|
|
49
|
+
* `LISTEN` — so a dedicated listener connection, which is exactly how the
|
|
50
|
+
* realtime engine works, received nothing while the *writer* received
|
|
51
|
+
* notifications it never asked for. `notification-proxy.ts` corrects that by
|
|
52
|
+
* copying every `NotificationResponse` frame to every client, which for a
|
|
53
|
+
* single-session database is simply the truth. Realtime therefore works
|
|
54
|
+
* against the managed database, with no change to the server: it does
|
|
55
|
+
* ordinary `LISTEN` over ordinary libpq.
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Extensions to hand PGlite's constructor.
|
|
59
|
+
*
|
|
60
|
+
* `CREATE EXTENSION` alone cannot install these — PGlite resolves them from
|
|
61
|
+
* bundles supplied at construction time, so anything missing here is missing
|
|
62
|
+
* from the database no matter what the migration says.
|
|
63
|
+
*/
|
|
64
|
+
var PGLITE_EXTENSION_NAMES = ["pg_trgm", "unaccent"];
|
|
65
|
+
/**
|
|
66
|
+
* Announced at startup, every time, rather than discovered.
|
|
67
|
+
*
|
|
68
|
+
* A developer who does not know realtime is off will read the silence as a bug
|
|
69
|
+
* in their own code, which is a worse outcome than not offering the managed
|
|
70
|
+
* database at all.
|
|
71
|
+
*/
|
|
72
|
+
var MANAGED_LIMITATIONS = [{
|
|
73
|
+
id: "concurrency",
|
|
74
|
+
summary: "Requests are served one at a time. Behaviour is correct but serialized, so lock contention and job-queue concurrency cannot be reproduced here.",
|
|
75
|
+
remedy: "Reproduce concurrency against a real Postgres: rebase dev --docker"
|
|
76
|
+
}];
|
|
77
|
+
//#endregion
|
|
78
|
+
export { PGLITE_EXTENSION_NAMES as n, MANAGED_LIMITATIONS as t };
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=constraints-BK1_4vci.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constraints-BK1_4vci.js","names":[],"sources":["../src/dev-db/constraints.ts"],"sourcesContent":["/**\n * What PGlite can and cannot do as a development database, measured rather\n * than assumed.\n *\n * Everything in this directory is shaped by four facts, each established by\n * running it against `@electric-sql/pglite` 0.5.6 and\n * `@electric-sql/pglite-socket` 0.2.9 rather than by reading their docs. They\n * are recorded here because two of them are silent failures — the kind that\n * make a developer lose an evening to a feature that reports success and does\n * nothing.\n *\n * 1. **It is really PostgreSQL 18.3.** `select version()` over the socket\n * returns `PostgreSQL 18.3 (PGlite 0.5.6) on wasm32`, which is the same\n * major as the `postgres:18-alpine` the eject template ships. So a dev\n * database here and a compose database there are the same Postgres, and\n * schema behaviour does not diverge between them.\n *\n * 2. **`pg_trgm` and `unaccent` are available**, which is what search\n * collections need. They are not installed by a bare `CREATE EXTENSION`,\n * though — PGlite ships them as separate bundles that must be passed to the\n * constructor, and without that `CREATE EXTENSION pg_trgm` fails with\n * `extension \"pg_trgm\" is not available`. {@link PGLITE_EXTENSIONS} is that\n * list, and it has to stay in step with what the schema generator emits.\n *\n * 3. **RLS is enforced exactly as it is on a real server.** With\n * `SET LOCAL ROLE \"rebase_user\"` inside a transaction — which is how\n * `PostgresBackendDriver` isolates every request — `current_user` becomes\n * the restricted role, `session_user` stays the owner, and a policy using\n * `current_setting('app.tenant')` filters rows correctly, including under\n * `FORCE ROW LEVEL SECURITY`. Measured: an owner saw 3 rows and the\n * role-switched transaction saw 2, with the cross-tenant probe returning 0.\n * This is the one that mattered most: a dev database that quietly failed to\n * apply RLS would give false confidence about the product's central claim.\n *\n * 4. **Concurrency is the real limit, and it fails badly.** PGlite is a single\n * session, and `PGLiteSocketServer` multiplexes connections onto it. Two\n * pooled clients that hold *overlapping transactions* deadlock — not error,\n * hang — which is precisely what a request-per-transaction server does under\n * any concurrent load. {@link MANAGED_POOL_MAX} is the answer: one client\n * connection, so requests queue in the pool instead of deadlocking in the\n * multiplexer. Measured: with a pool of 1, four concurrent queries and a\n * role-switched RLS transaction all pass; with a pool of 5 the same script\n * hangs indefinitely.\n *\n * 5. **LISTEN/NOTIFY needed repairing, and now works.** A notification is an\n * asynchronous message with no request to answer, and the multiplexer hands\n * it to whichever socket is reading rather than to the one that issued\n * `LISTEN` — so a dedicated listener connection, which is exactly how the\n * realtime engine works, received nothing while the *writer* received\n * notifications it never asked for. `notification-proxy.ts` corrects that by\n * copying every `NotificationResponse` frame to every client, which for a\n * single-session database is simply the truth. Realtime therefore works\n * against the managed database, with no change to the server: it does\n * ordinary `LISTEN` over ordinary libpq.\n */\n\n/**\n * Extensions to hand PGlite's constructor.\n *\n * `CREATE EXTENSION` alone cannot install these — PGlite resolves them from\n * bundles supplied at construction time, so anything missing here is missing\n * from the database no matter what the migration says.\n */\nexport const PGLITE_EXTENSION_NAMES = [\"pg_trgm\", \"unaccent\"] as const;\n\n/**\n * Client connections the managed database tolerates: exactly one.\n *\n * Not a tuning choice. Two concurrent transactions over the socket\n * multiplexer deadlock, and a request-per-transaction server produces those\n * the moment two requests overlap. One connection converts that deadlock into\n * ordinary queueing, which is slower and correct.\n */\nexport const MANAGED_POOL_MAX = 1;\n\n/**\n * Connections the socket server will accept.\n *\n * Above {@link MANAGED_POOL_MAX} so that a second *non-transactional* client —\n * `rebase db push` in another terminal while `rebase dev` runs — is refused\n * with a connection error rather than corrupting the multiplexer. The pool\n * limit is what prevents overlapping transactions; this only stops a stampede.\n */\nexport const MANAGED_SERVER_MAX_CONNECTIONS = 4;\n\n/** What a managed PGlite database cannot do, in the words the user needs. */\nexport interface ManagedLimitation {\n /** Stable id, so a warning can be suppressed or tested for. */\n id: string;\n /** One line, naming the feature rather than the mechanism. */\n summary: string;\n /** What to do instead. Always a concrete command. */\n remedy: string;\n}\n\n/**\n * Announced at startup, every time, rather than discovered.\n *\n * A developer who does not know realtime is off will read the silence as a bug\n * in their own code, which is a worse outcome than not offering the managed\n * database at all.\n */\nexport const MANAGED_LIMITATIONS: readonly ManagedLimitation[] = [\n {\n id: \"concurrency\",\n summary:\n \"Requests are served one at a time. Behaviour is correct but serialized, so \" +\n \"lock contention and job-queue concurrency cannot be reproduced here.\",\n remedy: \"Reproduce concurrency against a real Postgres: rebase dev --docker\"\n }\n] as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+DA,IAAa,yBAAyB,CAAC,WAAW,UAAU;;;;;;;;AAuC5D,IAAa,sBAAoD,CAC7D;CACI,IAAI;CACJ,SACI;CAEJ,QAAQ;AACZ,CACJ"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { a as findFreePort, c as releaseStartLock, f as __exportAll, i as devDbDir, l as stateFile, n as clearState, o as pidRunning, r as dataDir, s as readState, t as acquireStartLock } from "./state-c0CJ6Kwb.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import net from "net";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { randomBytes } from "crypto";
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
//#region src/dev-db/daemon.ts
|
|
9
|
+
/**
|
|
10
|
+
* Starting, finding and stopping the managed database, from the caller's side.
|
|
11
|
+
*
|
|
12
|
+
* Every command that needs Postgres calls {@link ensureManagedDatabase} and
|
|
13
|
+
* gets a connection string back. Whether that started a process or found one
|
|
14
|
+
* already running is not the caller's business, which is the point: `rebase
|
|
15
|
+
* db push`, `rebase dev` and `rebase studio` in three terminals must all reach
|
|
16
|
+
* the same database without coordinating, because two processes opening one
|
|
17
|
+
* PGlite data directory would corrupt it.
|
|
18
|
+
*
|
|
19
|
+
* The hard part is not starting the daemon; it is deciding whether the one the
|
|
20
|
+
* state file describes is still there. A pid can be recycled after a reboot and
|
|
21
|
+
* a port can be taken by a stranger, so believing either on its own would let
|
|
22
|
+
* Rebase send a migration somewhere unintended. {@link isDaemonAlive} asks the
|
|
23
|
+
* daemon to identify itself instead.
|
|
24
|
+
*/
|
|
25
|
+
var daemon_exports = /* @__PURE__ */ __exportAll({
|
|
26
|
+
ensureManagedDatabase: () => ensureManagedDatabase,
|
|
27
|
+
findRunningDaemon: () => findRunningDaemon,
|
|
28
|
+
isDaemonAlive: () => isDaemonAlive,
|
|
29
|
+
managedUrl: () => managedUrl,
|
|
30
|
+
resetManagedDatabase: () => resetManagedDatabase,
|
|
31
|
+
resolveSpawn: () => resolveSpawn,
|
|
32
|
+
stopManagedDatabase: () => stopManagedDatabase
|
|
33
|
+
});
|
|
34
|
+
/** How long to wait for a first boot. PGlite runs initdb on an empty data dir. */
|
|
35
|
+
var START_TIMEOUT_MS = 6e4;
|
|
36
|
+
/** Poll interval while waiting for the daemon to publish its state file. */
|
|
37
|
+
var POLL_INTERVAL_MS = 150;
|
|
38
|
+
/** How long the identity handshake may take before we call it dead. */
|
|
39
|
+
var IDENTITY_TIMEOUT_MS = 1500;
|
|
40
|
+
/**
|
|
41
|
+
* PGlite's built-in superuser and database.
|
|
42
|
+
*
|
|
43
|
+
* Fixed by PGlite rather than chosen here, and asserted by the socket spike:
|
|
44
|
+
* `session_user` comes back as `postgres`.
|
|
45
|
+
*/
|
|
46
|
+
var MANAGED_USER = "postgres";
|
|
47
|
+
var MANAGED_DATABASE = "postgres";
|
|
48
|
+
function managedUrl(port) {
|
|
49
|
+
return `postgresql://${MANAGED_USER}@127.0.0.1:${port}/${MANAGED_DATABASE}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Ask the process behind a state record to prove it is the one we wrote down.
|
|
53
|
+
*
|
|
54
|
+
* A pid check alone answers "is *a* process running", and a port check alone
|
|
55
|
+
* answers "is *something* listening" — after a reboot both say yes about
|
|
56
|
+
* strangers. The daemon answers with the token from its own state file, so a
|
|
57
|
+
* match is the only evidence accepted.
|
|
58
|
+
*/
|
|
59
|
+
function isDaemonAlive(state) {
|
|
60
|
+
if (!pidRunning(state.pid)) return Promise.resolve(false);
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
const socket = new net.Socket();
|
|
63
|
+
let answer = "";
|
|
64
|
+
const settle = (alive) => {
|
|
65
|
+
socket.removeAllListeners();
|
|
66
|
+
socket.destroy();
|
|
67
|
+
resolve(alive);
|
|
68
|
+
};
|
|
69
|
+
socket.setTimeout(IDENTITY_TIMEOUT_MS);
|
|
70
|
+
socket.once("timeout", () => settle(false));
|
|
71
|
+
socket.once("error", () => settle(false));
|
|
72
|
+
socket.on("data", (chunk) => {
|
|
73
|
+
answer += chunk.toString("utf8");
|
|
74
|
+
if (answer.includes("\n")) settle(answer.trim() === `rebase-dev-db ${state.token}`);
|
|
75
|
+
});
|
|
76
|
+
socket.once("close", () => settle(answer.trim() === `rebase-dev-db ${state.token}`));
|
|
77
|
+
socket.connect(state.identityPort, "127.0.0.1");
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** The running daemon for this project, or `null`. Never starts anything. */
|
|
81
|
+
async function findRunningDaemon(projectRoot) {
|
|
82
|
+
const state = readState(projectRoot);
|
|
83
|
+
if (!state) return null;
|
|
84
|
+
if (await isDaemonAlive(state)) return state;
|
|
85
|
+
clearState(projectRoot);
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the CLI entry point, so the daemon can be spawned as ourselves.
|
|
90
|
+
*
|
|
91
|
+
* A hidden subcommand rather than a second build entry: under `tsx` this file
|
|
92
|
+
* is `src/dev-db/daemon.ts` and in a published CLI it is bundled into
|
|
93
|
+
* `dist/index.js`, and in both cases the executable to re-invoke is the one
|
|
94
|
+
* already running.
|
|
95
|
+
*/
|
|
96
|
+
function resolveCliEntry() {
|
|
97
|
+
const argvEntry = process.argv[1];
|
|
98
|
+
if (argvEntry && fs.existsSync(argvEntry)) return argvEntry;
|
|
99
|
+
return fileURLToPath(new URL("data:video/mp2t;base64,ZXhwb3J0ICogZnJvbSAiLi9jbGkiOwpleHBvcnQgKiBmcm9tICIuL2NvbW1hbmRzL2luaXQiOwpleHBvcnQgKiBmcm9tICIuL2NvbW1hbmRzL3NjaGVtYSI7CmV4cG9ydCAqIGZyb20gIi4vY29tbWFuZHMvZGIiOwpleHBvcnQgKiBmcm9tICIuL2NvbW1hbmRzL2RldiI7CmV4cG9ydCAqIGZyb20gIi4vY29tbWFuZHMvYnVpbGQiOwpleHBvcnQgKiBmcm9tICIuL2NvbW1hbmRzL2VqZWN0IjsKZXhwb3J0ICogZnJvbSAiLi9jb21tYW5kcy9zdGFydCI7CmV4cG9ydCAqIGZyb20gIi4vY29tbWFuZHMvYXV0aCI7CmV4cG9ydCAqIGZyb20gIi4vY29tbWFuZHMvZG9jdG9yIjsKZXhwb3J0ICogZnJvbSAiLi9jb21tYW5kcy9nZW5lcmF0ZV9zZGsiOwpleHBvcnQgKiBmcm9tICIuL2NvbW1hbmRzL2Nsb3VkIjsKZXhwb3J0ICogZnJvbSAiLi9jb21tYW5kcy9hcHBzIjsKZXhwb3J0ICogZnJvbSAiLi91dGlscy9wcm9qZWN0IjsKZXhwb3J0ICogZnJvbSAiLi91dGlscy9wYWNrYWdlLW1hbmFnZXIiOwoKLy8gVGhlIG1hbmlmZXN0IGFuZCBidW5kbGUgY29udHJhY3RzLgovLwovLyBFeHBvcnRlZCBiZWNhdXNlIHZhbGlkYXRpbmcgdGhlbSBpcyBub3Qgb25seSB0aGUgQ0xJJ3Mgam9iOiBhIGNvbnRyb2wgcGxhbmUKLy8gaGFzIHRvIGRlY2lkZSB3aGV0aGVyIGEgc3VibWl0dGVkIGJ1bmRsZSBjYW4gcnVuIGJlZm9yZSBpdCBkZXBsb3lzIGl0LCBhbmQgaXQKLy8gbXVzdCByZWFjaCB0aGF0IHZlcmRpY3Qgd2l0aCB0aGUgc2FtZSBjb2RlIHRoYXQgcHJvZHVjZWQgdGhlIGFydGlmYWN0LgpleHBvcnQgKiBmcm9tICIuL21hbmlmZXN0IjsKZXhwb3J0ICogZnJvbSAiLi9idW5kbGUiOwo=", "" + import.meta.url));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* How to re-invoke ourselves, which differs between a published CLI and this
|
|
103
|
+
* repository.
|
|
104
|
+
*
|
|
105
|
+
* A published `rebase` is `node bin/rebase.js`, and re-running that is trivial.
|
|
106
|
+
* Inside the monorepo the entry is TypeScript, which plain `node` cannot load —
|
|
107
|
+
* so the daemon has to be started through the same loader that is running now.
|
|
108
|
+
* Getting this wrong fails as a spawn that exits instantly with a syntax error,
|
|
109
|
+
* which is why the caller reads `pglite.log` on failure.
|
|
110
|
+
*/
|
|
111
|
+
function resolveSpawn(entry) {
|
|
112
|
+
if (!/\.[cm]?ts$/.test(entry)) return {
|
|
113
|
+
execPath: process.execPath,
|
|
114
|
+
prefixArgs: []
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
execPath: process.execPath,
|
|
118
|
+
prefixArgs: ["--import", "tsx"]
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The project's managed database, started if it is not already running.
|
|
123
|
+
*
|
|
124
|
+
* Safe to call concurrently from several commands: the loser of the race finds
|
|
125
|
+
* the winner's state file during its poll and adopts it rather than starting a
|
|
126
|
+
* second daemon.
|
|
127
|
+
*/
|
|
128
|
+
async function ensureManagedDatabase(projectRoot, options = {}) {
|
|
129
|
+
const existing = await findRunningDaemon(projectRoot);
|
|
130
|
+
if (existing) return adopt(existing, false);
|
|
131
|
+
fs.mkdirSync(devDbDir(projectRoot), { recursive: true });
|
|
132
|
+
ensureGitignore(projectRoot);
|
|
133
|
+
if (!acquireStartLock(projectRoot, START_TIMEOUT_MS)) {
|
|
134
|
+
const adopted = await waitForDaemon(projectRoot, START_TIMEOUT_MS);
|
|
135
|
+
if (adopted) return adopt(adopted, false);
|
|
136
|
+
if (!acquireStartLock(projectRoot, 0)) throw new Error(`Another process is starting the development database and did not finish.
|
|
137
|
+
Remove ${path.join(devDbDir(projectRoot), "starting.lock")} if nothing is running.`);
|
|
138
|
+
}
|
|
139
|
+
const port = await findFreePort();
|
|
140
|
+
const token = randomBytes(16).toString("hex");
|
|
141
|
+
const entry = options.entry ?? resolveCliEntry();
|
|
142
|
+
const spawnPlan = options.spawn ?? resolveSpawn(entry);
|
|
143
|
+
const args = [
|
|
144
|
+
...spawnPlan.prefixArgs,
|
|
145
|
+
entry,
|
|
146
|
+
"__dev-db-daemon",
|
|
147
|
+
"--project",
|
|
148
|
+
projectRoot,
|
|
149
|
+
"--port",
|
|
150
|
+
String(port),
|
|
151
|
+
"--token",
|
|
152
|
+
token
|
|
153
|
+
];
|
|
154
|
+
if (options.idleTimeoutMs !== void 0) args.push("--idle-timeout", String(options.idleTimeoutMs));
|
|
155
|
+
const log = fs.openSync(path.join(devDbDir(projectRoot), "pglite.log"), "a");
|
|
156
|
+
const child = spawn(spawnPlan.execPath, args, {
|
|
157
|
+
detached: true,
|
|
158
|
+
stdio: [
|
|
159
|
+
"ignore",
|
|
160
|
+
log,
|
|
161
|
+
log
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
child.unref();
|
|
165
|
+
try {
|
|
166
|
+
const deadline = Date.now() + START_TIMEOUT_MS;
|
|
167
|
+
let announced = false;
|
|
168
|
+
while (Date.now() < deadline) {
|
|
169
|
+
const state = readState(projectRoot);
|
|
170
|
+
if (state && await isDaemonAlive(state)) return adopt(state, true);
|
|
171
|
+
if (!announced && !options.quiet) {
|
|
172
|
+
announced = true;
|
|
173
|
+
options.onProgress?.("Starting the development database…");
|
|
174
|
+
}
|
|
175
|
+
if (child.exitCode !== null && child.exitCode !== 0) throw new Error(`The development database failed to start (exit ${child.exitCode}).\n See ${path.join(devDbDir(projectRoot), "pglite.log")} for the reason.`);
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`The development database did not start within ${Math.round(START_TIMEOUT_MS / 1e3)}s.\n See ${path.join(devDbDir(projectRoot), "pglite.log")} for the reason.\n To use your own Postgres instead, set DATABASE_URL or pass --database-url.`);
|
|
179
|
+
} finally {
|
|
180
|
+
releaseStartLock(projectRoot);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Shape a live state record as the caller's result. */
|
|
184
|
+
function adopt(state, started) {
|
|
185
|
+
return {
|
|
186
|
+
url: managedUrl(state.port),
|
|
187
|
+
dataDir: state.dataDir,
|
|
188
|
+
port: state.port,
|
|
189
|
+
pid: state.pid,
|
|
190
|
+
started
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** Poll until another process publishes a live daemon, or give up. */
|
|
194
|
+
async function waitForDaemon(projectRoot, timeoutMs) {
|
|
195
|
+
const deadline = Date.now() + timeoutMs;
|
|
196
|
+
while (Date.now() < deadline) {
|
|
197
|
+
const state = readState(projectRoot);
|
|
198
|
+
if (state && await isDaemonAlive(state)) return state;
|
|
199
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/** Stop the daemon. Returns false when there was nothing running. */
|
|
204
|
+
async function stopManagedDatabase(projectRoot) {
|
|
205
|
+
const state = await findRunningDaemon(projectRoot);
|
|
206
|
+
if (!state) return false;
|
|
207
|
+
try {
|
|
208
|
+
process.kill(state.pid, "SIGTERM");
|
|
209
|
+
} catch {
|
|
210
|
+
clearState(projectRoot);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const deadline = Date.now() + 1e4;
|
|
214
|
+
while (Date.now() < deadline) {
|
|
215
|
+
if (!fs.existsSync(stateFile(projectRoot))) return true;
|
|
216
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
217
|
+
}
|
|
218
|
+
clearState(projectRoot);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Stop the daemon and delete the data directory.
|
|
223
|
+
*
|
|
224
|
+
* Destructive and deliberately not clever: it removes the whole directory
|
|
225
|
+
* rather than dropping schemas, because "give me an empty database" is the only
|
|
226
|
+
* thing anyone means by it.
|
|
227
|
+
*/
|
|
228
|
+
async function resetManagedDatabase(projectRoot) {
|
|
229
|
+
await stopManagedDatabase(projectRoot);
|
|
230
|
+
fs.rmSync(dataDir(projectRoot), {
|
|
231
|
+
recursive: true,
|
|
232
|
+
force: true
|
|
233
|
+
});
|
|
234
|
+
clearState(projectRoot);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Keep the generated directory out of git.
|
|
238
|
+
*
|
|
239
|
+
* `.rebase/` holds a Postgres data directory and a log. A project that commits
|
|
240
|
+
* it will push hundreds of megabytes and a database that only makes sense on
|
|
241
|
+
* one machine, so the ignore file is written next to the data rather than
|
|
242
|
+
* relying on the project's root `.gitignore` having been updated.
|
|
243
|
+
*/
|
|
244
|
+
function ensureGitignore(projectRoot) {
|
|
245
|
+
const target = path.join(devDbDir(projectRoot), ".gitignore");
|
|
246
|
+
if (fs.existsSync(target)) return;
|
|
247
|
+
fs.writeFileSync(target, "# Generated by `rebase dev` — a local Postgres data directory and its log.\n# Machine-specific and large; never commit it.\n*\n", "utf8");
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
export { ensureManagedDatabase as n, daemon_exports as t };
|
|
251
|
+
|
|
252
|
+
//# sourceMappingURL=daemon-Bdl4lrdt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-Bdl4lrdt.js","names":[],"sources":["../src/dev-db/daemon.ts"],"sourcesContent":["/**\n * Starting, finding and stopping the managed database, from the caller's side.\n *\n * Every command that needs Postgres calls {@link ensureManagedDatabase} and\n * gets a connection string back. Whether that started a process or found one\n * already running is not the caller's business, which is the point: `rebase\n * db push`, `rebase dev` and `rebase studio` in three terminals must all reach\n * the same database without coordinating, because two processes opening one\n * PGlite data directory would corrupt it.\n *\n * The hard part is not starting the daemon; it is deciding whether the one the\n * state file describes is still there. A pid can be recycled after a reboot and\n * a port can be taken by a stranger, so believing either on its own would let\n * Rebase send a migration somewhere unintended. {@link isDaemonAlive} asks the\n * daemon to identify itself instead.\n */\n\nimport { randomBytes } from \"crypto\";\nimport { type ChildProcess, spawn } from \"child_process\";\nimport fs from \"fs\";\nimport net from \"net\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nimport {\n acquireStartLock,\n clearState,\n type DaemonState,\n dataDir,\n devDbDir,\n findFreePort,\n pidRunning,\n readState,\n releaseStartLock,\n stateFile\n} from \"./state\";\n\n/** How long to wait for a first boot. PGlite runs initdb on an empty data dir. */\nconst START_TIMEOUT_MS = 60_000;\n/** Poll interval while waiting for the daemon to publish its state file. */\nconst POLL_INTERVAL_MS = 150;\n/** How long the identity handshake may take before we call it dead. */\nconst IDENTITY_TIMEOUT_MS = 1_500;\n\nexport interface ManagedDatabase {\n /** Connection string for this project's managed database. */\n url: string;\n /** Where the data lives, for diagnostics and `--reset`. */\n dataDir: string;\n port: number;\n pid: number;\n /** True when this call started the daemon rather than finding it. */\n started: boolean;\n}\n\n/**\n * PGlite's built-in superuser and database.\n *\n * Fixed by PGlite rather than chosen here, and asserted by the socket spike:\n * `session_user` comes back as `postgres`.\n */\nconst MANAGED_USER = \"postgres\";\nconst MANAGED_DATABASE = \"postgres\";\n\nexport function managedUrl(port: number): string {\n return `postgresql://${MANAGED_USER}@127.0.0.1:${port}/${MANAGED_DATABASE}`;\n}\n\n/**\n * Ask the process behind a state record to prove it is the one we wrote down.\n *\n * A pid check alone answers \"is *a* process running\", and a port check alone\n * answers \"is *something* listening\" — after a reboot both say yes about\n * strangers. The daemon answers with the token from its own state file, so a\n * match is the only evidence accepted.\n */\nexport function isDaemonAlive(state: DaemonState): Promise<boolean> {\n if (!pidRunning(state.pid)) return Promise.resolve(false);\n\n return new Promise((resolve) => {\n const socket = new net.Socket();\n let answer = \"\";\n const settle = (alive: boolean) => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(alive);\n };\n socket.setTimeout(IDENTITY_TIMEOUT_MS);\n socket.once(\"timeout\", () => settle(false));\n socket.once(\"error\", () => settle(false));\n socket.on(\"data\", (chunk) => {\n answer += chunk.toString(\"utf8\");\n if (answer.includes(\"\\n\")) settle(answer.trim() === `rebase-dev-db ${state.token}`);\n });\n socket.once(\"close\", () => settle(answer.trim() === `rebase-dev-db ${state.token}`));\n socket.connect(state.identityPort, \"127.0.0.1\");\n });\n}\n\n/** The running daemon for this project, or `null`. Never starts anything. */\nexport async function findRunningDaemon(projectRoot: string): Promise<DaemonState | null> {\n const state = readState(projectRoot);\n if (!state) return null;\n if (await isDaemonAlive(state)) return state;\n\n // The record describes something that is not there. Clearing it is the\n // whole recovery: the next ensure call starts a fresh daemon, and leaving\n // it would make every subsequent check pay the identity timeout.\n clearState(projectRoot);\n\n return null;\n}\n\n/**\n * Resolve the CLI entry point, so the daemon can be spawned as ourselves.\n *\n * A hidden subcommand rather than a second build entry: under `tsx` this file\n * is `src/dev-db/daemon.ts` and in a published CLI it is bundled into\n * `dist/index.js`, and in both cases the executable to re-invoke is the one\n * already running.\n */\nfunction resolveCliEntry(): string {\n // `process.argv[1]` is `bin/rebase.js` for a real invocation, which is\n // exactly what should be re-run. Under a test runner it is the runner, so\n // fall back to this module's own directory.\n const argvEntry = process.argv[1];\n if (argvEntry && fs.existsSync(argvEntry)) return argvEntry;\n\n return fileURLToPath(new URL(\"../index.js\", import.meta.url));\n}\n\n/**\n * How to re-invoke ourselves, which differs between a published CLI and this\n * repository.\n *\n * A published `rebase` is `node bin/rebase.js`, and re-running that is trivial.\n * Inside the monorepo the entry is TypeScript, which plain `node` cannot load —\n * so the daemon has to be started through the same loader that is running now.\n * Getting this wrong fails as a spawn that exits instantly with a syntax error,\n * which is why the caller reads `pglite.log` on failure.\n */\nexport function resolveSpawn(entry: string): { execPath: string; prefixArgs: string[] } {\n if (!/\\.[cm]?ts$/.test(entry)) return { execPath: process.execPath, prefixArgs: [] };\n\n // Node can load TypeScript when tsx is registered as an import hook. tsx is\n // already a dependency of this package, because `rebase dev` runs the\n // backend through it.\n return { execPath: process.execPath, prefixArgs: [\"--import\", \"tsx\"] };\n}\n\nexport interface EnsureOptions {\n /** Silence the \"starting…\" progress line. */\n quiet?: boolean;\n /** Milliseconds of inactivity before the daemon exits. 0 disables. */\n idleTimeoutMs?: number;\n /** Where progress goes. Injected for tests. */\n onProgress?: (message: string) => void;\n /** Override how the daemon process is launched. For tests. */\n spawn?: { execPath: string; prefixArgs: string[] };\n /**\n * Override the CLI entry to re-invoke. For tests.\n *\n * Necessary because under a test runner `process.argv[1]` is the runner\n * itself, which exists and is therefore accepted by {@link resolveCliEntry}\n * — spawning vitest with `__dev-db-daemon` rather than the CLI.\n */\n entry?: string;\n}\n\n/**\n * The project's managed database, started if it is not already running.\n *\n * Safe to call concurrently from several commands: the loser of the race finds\n * the winner's state file during its poll and adopts it rather than starting a\n * second daemon.\n */\nexport async function ensureManagedDatabase(\n projectRoot: string,\n options: EnsureOptions = {}\n): Promise<ManagedDatabase> {\n const existing = await findRunningDaemon(projectRoot);\n if (existing) return adopt(existing, false);\n\n fs.mkdirSync(devDbDir(projectRoot), { recursive: true });\n ensureGitignore(projectRoot);\n\n // Exactly one process may spawn a daemon. Without this, `rebase dev` and\n // `rebase db push` started in the same second both see no state file, both\n // spawn, and two processes open one PGlite data directory — the corruption\n // the single-daemon design exists to prevent.\n if (!acquireStartLock(projectRoot, START_TIMEOUT_MS)) {\n const adopted = await waitForDaemon(projectRoot, START_TIMEOUT_MS);\n if (adopted) return adopt(adopted, false);\n\n // The holder gave up or died without publishing. Falling through to\n // start one ourselves is better than failing: the lock is stale by now,\n // so the next acquire will break it.\n if (!acquireStartLock(projectRoot, 0)) {\n throw new Error(\n \"Another process is starting the development database and did not finish.\\n\" +\n ` Remove ${path.join(devDbDir(projectRoot), \"starting.lock\")} if nothing is running.`\n );\n }\n }\n\n const port = await findFreePort();\n const token = randomBytes(16).toString(\"hex\");\n const entry = options.entry ?? resolveCliEntry();\n\n const spawnPlan = options.spawn ?? resolveSpawn(entry);\n const args = [\n ...spawnPlan.prefixArgs,\n entry,\n \"__dev-db-daemon\",\n \"--project\", projectRoot,\n \"--port\", String(port),\n \"--token\", token\n ];\n if (options.idleTimeoutMs !== undefined) {\n args.push(\"--idle-timeout\", String(options.idleTimeoutMs));\n }\n\n const log = fs.openSync(path.join(devDbDir(projectRoot), \"pglite.log\"), \"a\");\n const child: ChildProcess = spawn(spawnPlan.execPath, args, {\n // Detached with no stdin and its output on a file: the daemon must\n // survive the command that started it, and must not hold the terminal\n // open when that command exits.\n detached: true,\n stdio: [\"ignore\", log, log]\n });\n child.unref();\n\n try {\n const deadline = Date.now() + START_TIMEOUT_MS;\n let announced = false;\n while (Date.now() < deadline) {\n const state = readState(projectRoot);\n if (state && (await isDaemonAlive(state))) return adopt(state, true);\n\n if (!announced && !options.quiet) {\n announced = true;\n options.onProgress?.(\"Starting the development database…\");\n }\n\n // A daemon that died on startup will never publish a state file, and\n // waiting the full minute for that is a bad way to learn it. The log\n // is the only place the reason exists.\n if (child.exitCode !== null && child.exitCode !== 0) {\n throw new Error(\n `The development database failed to start (exit ${child.exitCode}).\\n` +\n ` See ${path.join(devDbDir(projectRoot), \"pglite.log\")} for the reason.`\n );\n }\n\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n throw new Error(\n `The development database did not start within ${Math.round(START_TIMEOUT_MS / 1000)}s.\\n` +\n ` See ${path.join(devDbDir(projectRoot), \"pglite.log\")} for the reason.\\n` +\n \" To use your own Postgres instead, set DATABASE_URL or pass --database-url.\"\n );\n } finally {\n // Released whether we succeeded, timed out or threw. A lock left behind\n // by a crash is broken by age, but only after a full timeout — which a\n // developer should never have to wait out.\n releaseStartLock(projectRoot);\n }\n}\n\n/** Shape a live state record as the caller's result. */\nfunction adopt(state: DaemonState, started: boolean): ManagedDatabase {\n return {\n url: managedUrl(state.port),\n dataDir: state.dataDir,\n port: state.port,\n pid: state.pid,\n started\n };\n}\n\n/** Poll until another process publishes a live daemon, or give up. */\nasync function waitForDaemon(projectRoot: string, timeoutMs: number): Promise<DaemonState | null> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const state = readState(projectRoot);\n if (state && (await isDaemonAlive(state))) return state;\n await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));\n }\n\n return null;\n}\n\n/** Stop the daemon. Returns false when there was nothing running. */\nexport async function stopManagedDatabase(projectRoot: string): Promise<boolean> {\n const state = await findRunningDaemon(projectRoot);\n if (!state) return false;\n\n try {\n process.kill(state.pid, \"SIGTERM\");\n } catch {\n clearState(projectRoot);\n\n return false;\n }\n\n // Wait for it to clear its own state file, which is how it says it closed\n // the data directory cleanly. A hard kill here would risk the WAL.\n const deadline = Date.now() + 10_000;\n while (Date.now() < deadline) {\n if (!fs.existsSync(stateFile(projectRoot))) return true;\n await new Promise((resolve) => setTimeout(resolve, 100));\n }\n\n clearState(projectRoot);\n\n return true;\n}\n\n/**\n * Stop the daemon and delete the data directory.\n *\n * Destructive and deliberately not clever: it removes the whole directory\n * rather than dropping schemas, because \"give me an empty database\" is the only\n * thing anyone means by it.\n */\nexport async function resetManagedDatabase(projectRoot: string): Promise<void> {\n await stopManagedDatabase(projectRoot);\n fs.rmSync(dataDir(projectRoot), { recursive: true, force: true });\n clearState(projectRoot);\n}\n\n/**\n * Keep the generated directory out of git.\n *\n * `.rebase/` holds a Postgres data directory and a log. A project that commits\n * it will push hundreds of megabytes and a database that only makes sense on\n * one machine, so the ignore file is written next to the data rather than\n * relying on the project's root `.gitignore` having been updated.\n */\nfunction ensureGitignore(projectRoot: string): void {\n const target = path.join(devDbDir(projectRoot), \".gitignore\");\n if (fs.existsSync(target)) return;\n fs.writeFileSync(\n target,\n \"# Generated by `rebase dev` — a local Postgres data directory and its log.\\n\" +\n \"# Machine-specific and large; never commit it.\\n*\\n\",\n \"utf8\"\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,IAAM,mBAAmB;;AAEzB,IAAM,mBAAmB;;AAEzB,IAAM,sBAAsB;;;;;;;AAmB5B,IAAM,eAAe;AACrB,IAAM,mBAAmB;AAEzB,SAAgB,WAAW,MAAsB;CAC7C,OAAO,gBAAgB,aAAa,aAAa,KAAK,GAAG;AAC7D;;;;;;;;;AAUA,SAAgB,cAAc,OAAsC;CAChE,IAAI,CAAC,WAAW,MAAM,GAAG,GAAG,OAAO,QAAQ,QAAQ,KAAK;CAExD,OAAO,IAAI,SAAS,YAAY;EAC5B,MAAM,SAAS,IAAI,IAAI,OAAO;EAC9B,IAAI,SAAS;EACb,MAAM,UAAU,UAAmB;GAC/B,OAAO,mBAAmB;GAC1B,OAAO,QAAQ;GACf,QAAQ,KAAK;EACjB;EACA,OAAO,WAAW,mBAAmB;EACrC,OAAO,KAAK,iBAAiB,OAAO,KAAK,CAAC;EAC1C,OAAO,KAAK,eAAe,OAAO,KAAK,CAAC;EACxC,OAAO,GAAG,SAAS,UAAU;GACzB,UAAU,MAAM,SAAS,MAAM;GAC/B,IAAI,OAAO,SAAS,IAAI,GAAG,OAAO,OAAO,KAAK,MAAM,iBAAiB,MAAM,OAAO;EACtF,CAAC;EACD,OAAO,KAAK,eAAe,OAAO,OAAO,KAAK,MAAM,iBAAiB,MAAM,OAAO,CAAC;EACnF,OAAO,QAAQ,MAAM,cAAc,WAAW;CAClD,CAAC;AACL;;AAGA,eAAsB,kBAAkB,aAAkD;CACtF,MAAM,QAAQ,UAAU,WAAW;CACnC,IAAI,CAAC,OAAO,OAAO;CACnB,IAAI,MAAM,cAAc,KAAK,GAAG,OAAO;CAKvC,WAAW,WAAW;CAEtB,OAAO;AACX;;;;;;;;;AAUA,SAAS,kBAA0B;CAI/B,MAAM,YAAY,QAAQ,KAAK;CAC/B,IAAI,aAAa,GAAG,WAAW,SAAS,GAAG,OAAO;CAElD,OAAO,cAAc,IAAA,IAAA,unCAAA,KAAA,OAAA,KAAA,GAAA,CAAuC;AAChE;;;;;;;;;;;AAYA,SAAgB,aAAa,OAA2D;CACpF,IAAI,CAAC,aAAa,KAAK,KAAK,GAAG,OAAO;EAAE,UAAU,QAAQ;EAAU,YAAY,CAAC;CAAE;CAKnF,OAAO;EAAE,UAAU,QAAQ;EAAU,YAAY,CAAC,YAAY,KAAK;CAAE;AACzE;;;;;;;;AA4BA,eAAsB,sBAClB,aACA,UAAyB,CAAC,GACF;CACxB,MAAM,WAAW,MAAM,kBAAkB,WAAW;CACpD,IAAI,UAAU,OAAO,MAAM,UAAU,KAAK;CAE1C,GAAG,UAAU,SAAS,WAAW,GAAG,EAAE,WAAW,KAAK,CAAC;CACvD,gBAAgB,WAAW;CAM3B,IAAI,CAAC,iBAAiB,aAAa,gBAAgB,GAAG;EAClD,MAAM,UAAU,MAAM,cAAc,aAAa,gBAAgB;EACjE,IAAI,SAAS,OAAO,MAAM,SAAS,KAAK;EAKxC,IAAI,CAAC,iBAAiB,aAAa,CAAC,GAChC,MAAM,IAAI,MACN;WACY,KAAK,KAAK,SAAS,WAAW,GAAG,eAAe,EAAE,wBAClE;CAER;CAEA,MAAM,OAAO,MAAM,aAAa;CAChC,MAAM,QAAQ,YAAY,EAAE,CAAC,CAAC,SAAS,KAAK;CAC5C,MAAM,QAAQ,QAAQ,SAAS,gBAAgB;CAE/C,MAAM,YAAY,QAAQ,SAAS,aAAa,KAAK;CACrD,MAAM,OAAO;EACT,GAAG,UAAU;EACb;EACA;EACA;EAAa;EACb;EAAU,OAAO,IAAI;EACrB;EAAW;CACf;CACA,IAAI,QAAQ,kBAAkB,KAAA,GAC1B,KAAK,KAAK,kBAAkB,OAAO,QAAQ,aAAa,CAAC;CAG7D,MAAM,MAAM,GAAG,SAAS,KAAK,KAAK,SAAS,WAAW,GAAG,YAAY,GAAG,GAAG;CAC3E,MAAM,QAAsB,MAAM,UAAU,UAAU,MAAM;EAIxD,UAAU;EACV,OAAO;GAAC;GAAU;GAAK;EAAG;CAC9B,CAAC;CACD,MAAM,MAAM;CAEZ,IAAI;EACA,MAAM,WAAW,KAAK,IAAI,IAAI;EAC9B,IAAI,YAAY;EAChB,OAAO,KAAK,IAAI,IAAI,UAAU;GAC1B,MAAM,QAAQ,UAAU,WAAW;GACnC,IAAI,SAAU,MAAM,cAAc,KAAK,GAAI,OAAO,MAAM,OAAO,IAAI;GAEnE,IAAI,CAAC,aAAa,CAAC,QAAQ,OAAO;IAC9B,YAAY;IACZ,QAAQ,aAAa,oCAAoC;GAC7D;GAKA,IAAI,MAAM,aAAa,QAAQ,MAAM,aAAa,GAC9C,MAAM,IAAI,MACN,kDAAkD,MAAM,SAAS,YACxD,KAAK,KAAK,SAAS,WAAW,GAAG,YAAY,EAAE,iBAC5D;GAGJ,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,gBAAgB,CAAC;EACxE;EAEA,MAAM,IAAI,MACN,iDAAiD,KAAK,MAAM,mBAAmB,GAAI,EAAE,YAC5E,KAAK,KAAK,SAAS,WAAW,GAAG,YAAY,EAAE,+FAE5D;CACJ,UAAU;EAIN,iBAAiB,WAAW;CAChC;AACJ;;AAGA,SAAS,MAAM,OAAoB,SAAmC;CAClE,OAAO;EACH,KAAK,WAAW,MAAM,IAAI;EAC1B,SAAS,MAAM;EACf,MAAM,MAAM;EACZ,KAAK,MAAM;EACX;CACJ;AACJ;;AAGA,eAAe,cAAc,aAAqB,WAAgD;CAC9F,MAAM,WAAW,KAAK,IAAI,IAAI;CAC9B,OAAO,KAAK,IAAI,IAAI,UAAU;EAC1B,MAAM,QAAQ,UAAU,WAAW;EACnC,IAAI,SAAU,MAAM,cAAc,KAAK,GAAI,OAAO;EAClD,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,gBAAgB,CAAC;CACxE;CAEA,OAAO;AACX;;AAGA,eAAsB,oBAAoB,aAAuC;CAC7E,MAAM,QAAQ,MAAM,kBAAkB,WAAW;CACjD,IAAI,CAAC,OAAO,OAAO;CAEnB,IAAI;EACA,QAAQ,KAAK,MAAM,KAAK,SAAS;CACrC,QAAQ;EACJ,WAAW,WAAW;EAEtB,OAAO;CACX;CAIA,MAAM,WAAW,KAAK,IAAI,IAAI;CAC9B,OAAO,KAAK,IAAI,IAAI,UAAU;EAC1B,IAAI,CAAC,GAAG,WAAW,UAAU,WAAW,CAAC,GAAG,OAAO;EACnD,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC;CAC3D;CAEA,WAAW,WAAW;CAEtB,OAAO;AACX;;;;;;;;AASA,eAAsB,qBAAqB,aAAoC;CAC3E,MAAM,oBAAoB,WAAW;CACrC,GAAG,OAAO,QAAQ,WAAW,GAAG;EAAE,WAAW;EAAM,OAAO;CAAK,CAAC;CAChE,WAAW,WAAW;AAC1B;;;;;;;;;AAUA,SAAS,gBAAgB,aAA2B;CAChD,MAAM,SAAS,KAAK,KAAK,SAAS,WAAW,GAAG,YAAY;CAC5D,IAAI,GAAG,WAAW,MAAM,GAAG;CAC3B,GAAG,cACC,QACA,mIAEA,MACJ;AACJ"}
|