@rebasepro/cli 0.17.1 → 0.17.2-canary.g4d21a53
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/commands/cloud/action-help.d.ts +26 -0
- package/dist/commands/cloud/context.d.ts +22 -0
- package/dist/commands/cloud/databases.d.ts +48 -0
- package/dist/commands/cloud/deploy.d.ts +32 -8
- package/dist/commands/cloud/errors.d.ts +80 -0
- package/dist/commands/cloud/projects.d.ts +17 -0
- package/dist/commands/cloud/resources.d.ts +66 -0
- package/dist/constraints-DKGbNfUW.js +48 -0
- package/dist/constraints-DKGbNfUW.js.map +1 -0
- package/dist/{daemon-entry-Brq-S8XX.js → daemon-entry-CmJn83zu.js} +17 -7
- package/dist/daemon-entry-CmJn83zu.js.map +1 -0
- package/dist/dev-db/constraints.d.ts +17 -1
- package/dist/index.es.js +2319 -1604
- package/dist/index.es.js.map +1 -1
- package/package.json +7 -6
- package/dist/constraints-BK1_4vci.js +0 -80
- package/dist/constraints-BK1_4vci.js.map +0 -1
- package/dist/daemon-entry-Brq-S8XX.js.map +0 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface ActionHelp {
|
|
2
|
+
/** `cloud projects create` — no leading `rebase`. */
|
|
3
|
+
command: string;
|
|
4
|
+
/** The usage line, minus the `rebase ` prefix. */
|
|
5
|
+
usage: string;
|
|
6
|
+
/** One paragraph: what the command does, and what it does not. */
|
|
7
|
+
summary: string;
|
|
8
|
+
/** `[flag, description]`, in the order a reader needs them. */
|
|
9
|
+
flags: Array<[string, string]>;
|
|
10
|
+
examples: string[];
|
|
11
|
+
/** Anything a caller gets wrong more than once. */
|
|
12
|
+
notes?: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Flags every cloud command accepts, documented once.
|
|
16
|
+
*
|
|
17
|
+
* Excluded from the spec comparison below — they are merged in by
|
|
18
|
+
* `parseCloudArgs` for every command in the family, so repeating them per entry
|
|
19
|
+
* would be nine copies of the same four lines.
|
|
20
|
+
*/
|
|
21
|
+
export declare const GLOBAL_HELP_FLAGS: Array<[string, string]>;
|
|
22
|
+
/** Flag names that `parseCloudArgs` adds to every command in the family. */
|
|
23
|
+
export declare const GLOBAL_SPEC_KEYS: Set<string>;
|
|
24
|
+
export declare const ACTION_HELP: Record<string, ActionHelp>;
|
|
25
|
+
/** Print one action's page — human, or its JSON description when piped. */
|
|
26
|
+
export declare function printActionHelp(entry: ActionHelp): void;
|
|
@@ -347,6 +347,22 @@ export declare function parseCloudArgs<S extends arg.Spec>(opts: {
|
|
|
347
347
|
flags: arg.Result<S & typeof GLOBAL_CLOUD_FLAGS>;
|
|
348
348
|
positionals: string[];
|
|
349
349
|
};
|
|
350
|
+
/**
|
|
351
|
+
* `--timeout <seconds>` as milliseconds, or `fallbackMs` when it was not given.
|
|
352
|
+
*
|
|
353
|
+
* One function rather than one per command, because two commands take this flag
|
|
354
|
+
* and a second copy is where the two would come to disagree about what
|
|
355
|
+
* `--timeout 0` means.
|
|
356
|
+
*
|
|
357
|
+
* A value this cannot read is a refusal, not a fall back to the default. The
|
|
358
|
+
* whole reason a caller passes a timeout is that it has a deadline of its own;
|
|
359
|
+
* quietly substituting a different one is how a fifteen-minute wait turns up
|
|
360
|
+
* inside a five-minute CI step, having been asked for `--timeout 30s`.
|
|
361
|
+
*/
|
|
362
|
+
export declare function resolveTimeoutMs(value: string | undefined, opts: {
|
|
363
|
+
fallbackMs: number;
|
|
364
|
+
command: string;
|
|
365
|
+
}): number;
|
|
350
366
|
/**
|
|
351
367
|
* Announce an outcome — "Logged in as …", "Deleted project …".
|
|
352
368
|
*
|
|
@@ -392,6 +408,12 @@ export declare function keyValues(rows: Array<[string, string | null | undefined
|
|
|
392
408
|
/**
|
|
393
409
|
* Surface an SDK/HTTP error consistently. The SDK throws RebaseApiError with
|
|
394
410
|
* a `.status` and `.message`; anything else falls back to its string form.
|
|
411
|
+
*
|
|
412
|
+
* The message is summarised rather than printed — see `summarizeError`. What
|
|
413
|
+
* arrives here is routinely a whole Kubernetes `Status` object with the request
|
|
414
|
+
* headers appended, and the one sentence worth reading is inside it. The
|
|
415
|
+
* untouched body is still available, behind `--debug`, on stderr where it
|
|
416
|
+
* cannot corrupt the JSON value on stdout.
|
|
395
417
|
*/
|
|
396
418
|
export declare function reportError(e: unknown, context: string): never;
|
|
397
419
|
/**
|
|
@@ -7,7 +7,54 @@
|
|
|
7
7
|
* db backup list|create|restore
|
|
8
8
|
*/
|
|
9
9
|
import arg from "arg";
|
|
10
|
+
import { type CloudClient } from "./context.js";
|
|
11
|
+
interface DatabaseRow {
|
|
12
|
+
id: string | number;
|
|
13
|
+
type?: string;
|
|
14
|
+
connectionStatus?: string;
|
|
15
|
+
useSshTunnel?: boolean;
|
|
16
|
+
pitrEnabled?: boolean;
|
|
17
|
+
}
|
|
10
18
|
export declare function dbCommand(subcommand: string | undefined, rawArgs: string[]): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Attach a database row to a project.
|
|
21
|
+
*
|
|
22
|
+
* Extracted so `rebase cloud projects create` can do it in the same breath as
|
|
23
|
+
* creating the project — see `--db` there. Two call sites, one insert, so the
|
|
24
|
+
* shape of the row cannot drift between "attached at creation" and "attached
|
|
25
|
+
* afterwards".
|
|
26
|
+
*/
|
|
27
|
+
export declare function attachDatabaseRow(client: CloudClient, input: {
|
|
28
|
+
projectId: string;
|
|
29
|
+
type: string;
|
|
30
|
+
connectionString?: string;
|
|
31
|
+
}): Promise<DatabaseRow>;
|
|
32
|
+
/** What `rebase cloud db create` parses. Exported so its help page cannot drift. */
|
|
33
|
+
export declare const CREATE_DATABASE_FLAGS: {
|
|
34
|
+
readonly "--type": StringConstructor;
|
|
35
|
+
readonly "--connection-string": StringConstructor;
|
|
36
|
+
readonly "--wait": BooleanConstructor;
|
|
37
|
+
readonly "--timeout": StringConstructor;
|
|
38
|
+
readonly "--project": StringConstructor;
|
|
39
|
+
readonly "-p": "--project";
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Wait for an attached database to become usable, where "usable" means
|
|
43
|
+
* something.
|
|
44
|
+
*
|
|
45
|
+
* Returns `waited: false` for the managed case, and says why — the caller then
|
|
46
|
+
* knows the state it is looking at is final rather than early.
|
|
47
|
+
*/
|
|
48
|
+
export declare function waitForDatabase(client: CloudClient, opts: {
|
|
49
|
+
projectId: string;
|
|
50
|
+
type: string;
|
|
51
|
+
timeoutMs: number;
|
|
52
|
+
pollMs?: number;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
waited: boolean;
|
|
55
|
+
connectionStatus: string;
|
|
56
|
+
note?: string;
|
|
57
|
+
}>;
|
|
11
58
|
/**
|
|
12
59
|
* `db backup [action] [filename]`, resolved in one strict parse.
|
|
13
60
|
*
|
|
@@ -39,3 +86,4 @@ export declare function resolveBackupArgs(rawArgs: string[]): {
|
|
|
39
86
|
filename: string;
|
|
40
87
|
};
|
|
41
88
|
export declare function printDbHelp(): void;
|
|
89
|
+
export {};
|
|
@@ -124,6 +124,26 @@ export declare function ejectRefusal(opts: EjectContext, projectRef: string): {
|
|
|
124
124
|
export declare function deployWarnings(opts: EjectContext, projectRef: string): DeployWarning[];
|
|
125
125
|
/** The warning half of a deploy's JSON payload — merged into whatever it emits. */
|
|
126
126
|
export declare function warningPayload(warnings: DeployWarning[]): Record<string, unknown>;
|
|
127
|
+
/**
|
|
128
|
+
* Every flag `rebase cloud deploy` accepts.
|
|
129
|
+
*
|
|
130
|
+
* Hoisted out of the `parseCloudArgs` call so that one declaration serves three
|
|
131
|
+
* readers: the parser, `action-help.ts`'s page for this command, and the test
|
|
132
|
+
* that holds the two to each other. A flag added here with no line in the help
|
|
133
|
+
* page is a failing test rather than a flag nobody can discover.
|
|
134
|
+
*/
|
|
135
|
+
export declare const DEPLOY_FLAGS: {
|
|
136
|
+
readonly "--no-follow": BooleanConstructor;
|
|
137
|
+
readonly "--wait": BooleanConstructor;
|
|
138
|
+
readonly "--timeout": StringConstructor;
|
|
139
|
+
readonly "--source": StringConstructor;
|
|
140
|
+
readonly "--message": StringConstructor;
|
|
141
|
+
readonly "--bundle": BooleanConstructor;
|
|
142
|
+
readonly "--bundle-dir": StringConstructor;
|
|
143
|
+
readonly "--skip-type-check": BooleanConstructor;
|
|
144
|
+
readonly "--force": BooleanConstructor;
|
|
145
|
+
readonly "-m": "--message";
|
|
146
|
+
};
|
|
127
147
|
/**
|
|
128
148
|
* `rebase cloud deploy [app]` — its flags, and which app of this repository the
|
|
129
149
|
* line named.
|
|
@@ -148,14 +168,16 @@ export declare function warningPayload(warnings: DeployWarning[]): Record<string
|
|
|
148
168
|
*/
|
|
149
169
|
export declare function resolveDeployArgs(rawArgs: string[]): {
|
|
150
170
|
flags: arg.Result<{
|
|
151
|
-
"--no-follow": BooleanConstructor;
|
|
152
|
-
"--
|
|
153
|
-
"--
|
|
154
|
-
"--
|
|
155
|
-
"--
|
|
156
|
-
"--
|
|
157
|
-
"--
|
|
158
|
-
"-
|
|
171
|
+
readonly "--no-follow": BooleanConstructor;
|
|
172
|
+
readonly "--wait": BooleanConstructor;
|
|
173
|
+
readonly "--timeout": StringConstructor;
|
|
174
|
+
readonly "--source": StringConstructor;
|
|
175
|
+
readonly "--message": StringConstructor;
|
|
176
|
+
readonly "--bundle": BooleanConstructor;
|
|
177
|
+
readonly "--bundle-dir": StringConstructor;
|
|
178
|
+
readonly "--skip-type-check": BooleanConstructor;
|
|
179
|
+
readonly "--force": BooleanConstructor;
|
|
180
|
+
readonly "-m": "--message";
|
|
159
181
|
} & {
|
|
160
182
|
readonly "--json": BooleanConstructor;
|
|
161
183
|
readonly "--yes": BooleanConstructor;
|
|
@@ -169,4 +191,6 @@ export declare function resolveDeployArgs(rawArgs: string[]): {
|
|
|
169
191
|
appName: string | undefined;
|
|
170
192
|
};
|
|
171
193
|
export declare function deployCommand(rawArgs: string[], projectRef: string): Promise<void>;
|
|
194
|
+
/** `--timeout <seconds>` for a deploy, or the 15-minute default. */
|
|
195
|
+
export declare function resolveDeployTimeout(value: string | undefined): number;
|
|
172
196
|
export declare function logsCommand(rawArgs: string[], projectRef: string): Promise<void>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning a control-plane failure into something the caller can act on.
|
|
3
|
+
*
|
|
4
|
+
* The control plane talks to Kubernetes, and when Kubernetes refuses it, the
|
|
5
|
+
* refusal travels back verbatim: a whole `Status` object, the request headers,
|
|
6
|
+
* an `audit-id`, an `x-kubernetes-pf-flowschema-uid`. That reached the user's
|
|
7
|
+
* terminal unedited. Two things are wrong with it beyond the noise.
|
|
8
|
+
*
|
|
9
|
+
* The first is that the one sentence that matters is buried in the middle of a
|
|
10
|
+
* JSON blob, so the remedy — if there is one — is the hardest part to find.
|
|
11
|
+
*
|
|
12
|
+
* The second is worse, and is the reason this file exists rather than a
|
|
13
|
+
* `slice(0, 200)`. A `403` naming a `system:serviceaccount:` is a statement
|
|
14
|
+
* about the PLATFORM's own credentials: some role the control plane runs as is
|
|
15
|
+
* missing a grant. Nothing in the user's project can change that — not the
|
|
16
|
+
* collections, not `rebase.json`, not the deploy flags — and the error, printed
|
|
17
|
+
* raw in the middle of `rebase cloud deploy`, reads exactly like a project
|
|
18
|
+
* fault. Someone acting on that reading deletes working code looking for the
|
|
19
|
+
* cause. (That is not hypothetical: three cron jobs were removed from a project
|
|
20
|
+
* to test whether they caused a `cronjobs.batch` 403. They did not.)
|
|
21
|
+
*
|
|
22
|
+
* So this classifies before it summarises, and says whose problem it is.
|
|
23
|
+
*/
|
|
24
|
+
/** What a Kubernetes API refusal carries, once it is found. */
|
|
25
|
+
export interface KubernetesStatus {
|
|
26
|
+
message?: string;
|
|
27
|
+
reason?: string;
|
|
28
|
+
code?: number;
|
|
29
|
+
details?: {
|
|
30
|
+
group?: string;
|
|
31
|
+
kind?: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface ErrorSummary {
|
|
36
|
+
/** One line, safe to print. Never the raw body. */
|
|
37
|
+
message: string;
|
|
38
|
+
/** A remedy, when one exists — or the reason there is none. */
|
|
39
|
+
hint?: string;
|
|
40
|
+
/** A stable code for `--json` callers to branch on. */
|
|
41
|
+
code: string;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the failure is the platform's rather than the project's.
|
|
44
|
+
*
|
|
45
|
+
* The whole point of the summary: a caller that retries or mutates the
|
|
46
|
+
* project on a platform-side refusal makes things worse, and an agent
|
|
47
|
+
* cannot tell from the prose.
|
|
48
|
+
*/
|
|
49
|
+
platform: boolean;
|
|
50
|
+
/** The untouched original, printed only under `--debug`. */
|
|
51
|
+
raw: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A Kubernetes `Status` object embedded anywhere in a message.
|
|
55
|
+
*
|
|
56
|
+
* Scanned for rather than parsed off the front: the control plane wraps it
|
|
57
|
+
* ("Failed to create the tenant namespace: …"), the client library appends
|
|
58
|
+
* headers after it, and both halves are worth keeping out of the summary.
|
|
59
|
+
* Balanced-brace scanning rather than a regex, because `details.causes` nests
|
|
60
|
+
* and a lazy `\{.*?\}` truncates the object at the first inner brace — which
|
|
61
|
+
* parses to nothing and silently falls through to the raw message.
|
|
62
|
+
*/
|
|
63
|
+
export declare function extractKubernetesStatus(text: string): KubernetesStatus | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* One actionable line (plus a hint) from whatever the control plane returned.
|
|
66
|
+
*
|
|
67
|
+
* `status` is the HTTP status of the control-plane call itself, which is a
|
|
68
|
+
* different number from the `code` inside an embedded Kubernetes `Status` — the
|
|
69
|
+
* control plane routinely answers 500 while the cluster answered 403, and it is
|
|
70
|
+
* the inner one that says what happened.
|
|
71
|
+
*/
|
|
72
|
+
export declare function summarizeError(error: unknown, context: string): ErrorSummary;
|
|
73
|
+
/**
|
|
74
|
+
* Whether the caller asked for the untouched body.
|
|
75
|
+
*
|
|
76
|
+
* `--debug` is already what `bin/rebase.js` prints after every failure as the
|
|
77
|
+
* thing to add, so the raw payload hangs off the flag people are told to reach
|
|
78
|
+
* for rather than off one invented here.
|
|
79
|
+
*/
|
|
80
|
+
export declare function wantsRawError(argv?: readonly string[]): boolean;
|
|
@@ -42,6 +42,23 @@ export declare const CREATE_PROJECT_FLAGS: {
|
|
|
42
42
|
readonly "--region": StringConstructor;
|
|
43
43
|
readonly "--org": StringConstructor;
|
|
44
44
|
readonly "--link": BooleanConstructor;
|
|
45
|
+
/**
|
|
46
|
+
* Which database the new project gets — `managed` (the default), `byodb`,
|
|
47
|
+
* or `none`.
|
|
48
|
+
*
|
|
49
|
+
* A default rather than a prompt, and `managed` rather than `none`, because
|
|
50
|
+
* the state this removes is not a missing convenience: a project with no
|
|
51
|
+
* database is written `status: "provisioning"` and can never deploy, and
|
|
52
|
+
* nothing in that word says a second command is owed. Attaching one here
|
|
53
|
+
* means the two-command sequence that every project needs is one command,
|
|
54
|
+
* and `--db none` is there for the case that genuinely wants to decide later.
|
|
55
|
+
*
|
|
56
|
+
* Distinct from `--db-mode`/`--db-cpu` next to it, which are resource dials
|
|
57
|
+
* on a database that exists. This is whether there is one.
|
|
58
|
+
*/
|
|
59
|
+
readonly "--db": StringConstructor;
|
|
60
|
+
/** For `--db byodb`. Same spelling as `rebase cloud db create` uses. */
|
|
61
|
+
readonly "--connection-string": StringConstructor;
|
|
45
62
|
readonly "-n": "--name";
|
|
46
63
|
readonly "--cpu": StringConstructor;
|
|
47
64
|
readonly "--memory": StringConstructor;
|
|
@@ -35,6 +35,41 @@ export declare function describeStorageState(state: StorageState | undefined): s
|
|
|
35
35
|
* type; the verdict appears once there is one.
|
|
36
36
|
*/
|
|
37
37
|
export declare function describeDatabaseState(db: Record<string, unknown> | undefined): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The one thing standing between this project and a live URL, if anything is.
|
|
40
|
+
*
|
|
41
|
+
* `status` alone cannot answer that, and the gap is not cosmetic. A project
|
|
42
|
+
* created through `projects create` is written `status: "provisioning"` and has
|
|
43
|
+
* no database until one is attached — so the platform is not provisioning
|
|
44
|
+
* anything, it is waiting for a second command that nothing in the output names.
|
|
45
|
+
* "Provisioning" reads as *work in progress*, and the correct response to work
|
|
46
|
+
* in progress is to wait. So the correct response to this state, for a person
|
|
47
|
+
* and an agent alike, is the one thing that is guaranteed never to resolve it.
|
|
48
|
+
*
|
|
49
|
+
* That cost 43 minutes of polling on a first deploy, and an unattended agent
|
|
50
|
+
* would still be polling: nothing about the state changes, ever, so there is no
|
|
51
|
+
* timeout short enough to be wrong and no timeout long enough to be right.
|
|
52
|
+
*
|
|
53
|
+
* `blockedOn: null` is therefore load-bearing — it is the CLI saying "waiting is
|
|
54
|
+
* the correct thing to do here", which is the only condition under which a
|
|
55
|
+
* caller should poll. Everything else names a command.
|
|
56
|
+
*/
|
|
57
|
+
export interface BlockedState {
|
|
58
|
+
/** A stable slug, or `null` when the platform genuinely is working. */
|
|
59
|
+
blockedOn: string | null;
|
|
60
|
+
/** The exact command that unblocks it. `null` when nothing is blocked. */
|
|
61
|
+
nextAction: string | null;
|
|
62
|
+
}
|
|
63
|
+
export declare function resolveBlockedState(input: {
|
|
64
|
+
projectStatus?: string | null;
|
|
65
|
+
/** The project's database row, or undefined when none is attached. */
|
|
66
|
+
database?: {
|
|
67
|
+
connectionStatus?: string | null;
|
|
68
|
+
} | undefined;
|
|
69
|
+
lastDeploy?: {
|
|
70
|
+
status?: string | null;
|
|
71
|
+
} | undefined;
|
|
72
|
+
}): BlockedState;
|
|
38
73
|
/**
|
|
39
74
|
* One line describing what engine is serving this project.
|
|
40
75
|
*
|
|
@@ -82,6 +117,37 @@ export declare function printStorageHelp(): void;
|
|
|
82
117
|
* cluster" flow is a different feature with a different threat model.
|
|
83
118
|
*/
|
|
84
119
|
export declare function clustersCommand(action: string | undefined, rawArgs: string[]): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Ask a registered cluster whether it can actually host a tenant.
|
|
122
|
+
*
|
|
123
|
+
* The question this exists to answer early is the one that otherwise gets
|
|
124
|
+
* answered by a customer's first deploy failing halfway through provisioning,
|
|
125
|
+
* with an error they cannot act on and half a tenant already created.
|
|
126
|
+
*/
|
|
127
|
+
/**
|
|
128
|
+
* Which cluster `clusters verify` was asked about, and whether `--baseline`
|
|
129
|
+
* was given.
|
|
130
|
+
*
|
|
131
|
+
* Resolved against a real spec, not scanned out of `rawArgs` by hand — and this
|
|
132
|
+
* is not a tidy-up. `rawArgs` is the whole `process.argv`, so the old scan
|
|
133
|
+
* ("the first token that is not `--…` and is neither `clusters` nor `verify`")
|
|
134
|
+
* matched `argv[0]`, the **node binary path**. Every `rebase cloud clusters
|
|
135
|
+
* verify <id>` therefore asked the control plane about a cluster called
|
|
136
|
+
* `/usr/local/bin/node`, and came back 404.
|
|
137
|
+
*
|
|
138
|
+
* So the one diagnostic that reports `permissions.allowed` /
|
|
139
|
+
* `permissions.denied` was unreachable, and its 404 read as "this command is
|
|
140
|
+
* not deployed yet" rather than "the id never left this machine intact". It is
|
|
141
|
+
* the command that names a missing `cronjobs.batch` grant in a single call
|
|
142
|
+
* instead of a twenty-minute A/B against a live project.
|
|
143
|
+
*
|
|
144
|
+
* Same failure as `cloud deploy` reading `_[0]` as `"cloud"`, and the same fix:
|
|
145
|
+
* one parser, exported so its test drives the real thing rather than a copy.
|
|
146
|
+
*/
|
|
147
|
+
export declare function resolveClusterVerifyArgs(rawArgs: string[]): {
|
|
148
|
+
id?: string;
|
|
149
|
+
baseline: boolean;
|
|
150
|
+
};
|
|
85
151
|
export declare function billingCommand(rawArgs: string[]): Promise<void>;
|
|
86
152
|
/**
|
|
87
153
|
* `rebase cloud resources` — show what a project is given, and change it.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/dev-db/constraints.ts
|
|
2
|
+
/**
|
|
3
|
+
* Extensions to hand PGlite's constructor.
|
|
4
|
+
*
|
|
5
|
+
* `CREATE EXTENSION` alone cannot install these — PGlite resolves them from
|
|
6
|
+
* bundles supplied at construction time, so anything missing here is missing
|
|
7
|
+
* from the database no matter what the migration says.
|
|
8
|
+
*
|
|
9
|
+
* The module is spelled out per extension rather than derived from the name,
|
|
10
|
+
* because they do not all live in the same place: the two contrib ones are
|
|
11
|
+
* subpaths of PGlite itself, and pgvector is a package of its own. Deriving the
|
|
12
|
+
* path is what left `vector` off this list — `@electric-sql/pglite/contrib/vector`
|
|
13
|
+
* does not exist, so a project declaring a `{ type: "vector" }` property could
|
|
14
|
+
* not use the managed dev database at all.
|
|
15
|
+
*/
|
|
16
|
+
var PGLITE_EXTENSIONS = [
|
|
17
|
+
{
|
|
18
|
+
name: "pg_trgm",
|
|
19
|
+
module: "@electric-sql/pglite/contrib/pg_trgm",
|
|
20
|
+
export: "pg_trgm"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: "unaccent",
|
|
24
|
+
module: "@electric-sql/pglite/contrib/unaccent",
|
|
25
|
+
export: "unaccent"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "vector",
|
|
29
|
+
module: "@electric-sql/pglite-pgvector",
|
|
30
|
+
export: "vector"
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
/**
|
|
34
|
+
* Announced at startup, every time, rather than discovered.
|
|
35
|
+
*
|
|
36
|
+
* A developer who does not know realtime is off will read the silence as a bug
|
|
37
|
+
* in their own code, which is a worse outcome than not offering the managed
|
|
38
|
+
* database at all.
|
|
39
|
+
*/
|
|
40
|
+
var MANAGED_LIMITATIONS = [{
|
|
41
|
+
id: "concurrency",
|
|
42
|
+
summary: "Requests are served one at a time. Behaviour is correct but serialized, so lock contention and job-queue concurrency cannot be reproduced here.",
|
|
43
|
+
remedy: "Reproduce concurrency against a real Postgres: rebase dev --docker"
|
|
44
|
+
}];
|
|
45
|
+
//#endregion
|
|
46
|
+
export { PGLITE_EXTENSIONS as n, MANAGED_LIMITATIONS as t };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=constraints-DKGbNfUW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constraints-DKGbNfUW.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/** One extension bundle, and where to import it from. */\nexport interface PgliteExtension {\n /** The name `CREATE EXTENSION` uses, and the key PGlite is handed. */\n name: string;\n /** Module the bundle is imported from. */\n module: string;\n /** The named export carrying the bundle. */\n export: string;\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 *\n * The module is spelled out per extension rather than derived from the name,\n * because they do not all live in the same place: the two contrib ones are\n * subpaths of PGlite itself, and pgvector is a package of its own. Deriving the\n * path is what left `vector` off this list — `@electric-sql/pglite/contrib/vector`\n * does not exist, so a project declaring a `{ type: \"vector\" }` property could\n * not use the managed dev database at all.\n */\nexport const PGLITE_EXTENSIONS: readonly PgliteExtension[] = [\n { name: \"pg_trgm\", module: \"@electric-sql/pglite/contrib/pg_trgm\", export: \"pg_trgm\" },\n { name: \"unaccent\", module: \"@electric-sql/pglite/contrib/unaccent\", export: \"unaccent\" },\n { name: \"vector\", module: \"@electric-sql/pglite-pgvector\", export: \"vector\" }\n];\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":";;;;;;;;;;;;;;;AAgFA,IAAa,oBAAgD;CACzD;EAAE,MAAM;EAAW,QAAQ;EAAwC,QAAQ;CAAU;CACrF;EAAE,MAAM;EAAY,QAAQ;EAAyC,QAAQ;CAAW;CACxF;EAAE,MAAM;EAAU,QAAQ;EAAiC,QAAQ;CAAS;AAChF;;;;;;;;AAuCA,IAAa,sBAAoD,CAC7D;CACI,IAAI;CACJ,SACI;CAEJ,QAAQ;AACZ,CACJ"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as findFreePort, d as writeState, n as clearState, r as dataDir } from "./state-c0CJ6Kwb.js";
|
|
2
|
-
import { n as
|
|
2
|
+
import { n as PGLITE_EXTENSIONS } from "./constraints-DKGbNfUW.js";
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import net from "net";
|
|
5
5
|
//#region src/dev-db/notification-proxy.ts
|
|
@@ -262,19 +262,29 @@ function parseDaemonArgs(argv) {
|
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
/**
|
|
265
|
-
* Load the extension bundles PGlite needs
|
|
265
|
+
* Load the extension bundles PGlite needs.
|
|
266
266
|
*
|
|
267
267
|
* `CREATE EXTENSION pg_trgm` cannot install anything on its own here — PGlite
|
|
268
268
|
* resolves extensions from bundles handed to the constructor, and a missing one
|
|
269
269
|
* fails at migration time with `extension "pg_trgm" is not available`, which
|
|
270
270
|
* reads like a broken database rather than a missing import.
|
|
271
|
+
*
|
|
272
|
+
* Every failure here is loud for that reason, including the one that is new:
|
|
273
|
+
* pgvector arrives from a package of its own, so unlike the contrib bundles it
|
|
274
|
+
* can be absent while PGlite itself is fine.
|
|
271
275
|
*/
|
|
272
276
|
async function loadExtensions() {
|
|
273
277
|
const extensions = {};
|
|
274
|
-
for (const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
+
for (const extension of PGLITE_EXTENSIONS) {
|
|
279
|
+
let module;
|
|
280
|
+
try {
|
|
281
|
+
module = await import(extension.module);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
throw new Error(`Could not load "${extension.module}", which supplies the ${extension.name} extension: ${err instanceof Error ? err.message : String(err)}\nIt is an optional dependency of @rebasepro/cli — reinstall the project, or run \`rebase dev --docker\` to use a real Postgres instead.`);
|
|
284
|
+
}
|
|
285
|
+
const bundle = module[extension.export];
|
|
286
|
+
if (!bundle) throw new Error(`${extension.module} did not export "${extension.export}". The installed PGlite version may not ship this extension.`);
|
|
287
|
+
extensions[extension.name] = bundle;
|
|
278
288
|
}
|
|
279
289
|
return extensions;
|
|
280
290
|
}
|
|
@@ -375,4 +385,4 @@ async function runDaemon(args) {
|
|
|
375
385
|
//#endregion
|
|
376
386
|
export { parseDaemonArgs, runDaemon };
|
|
377
387
|
|
|
378
|
-
//# sourceMappingURL=daemon-entry-
|
|
388
|
+
//# sourceMappingURL=daemon-entry-CmJn83zu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-entry-CmJn83zu.js","names":[],"sources":["../src/dev-db/notification-proxy.ts","../src/dev-db/daemon-entry.ts"],"sourcesContent":["/**\n * A transparent Postgres proxy that puts LISTEN/NOTIFY back.\n *\n * Without this, realtime does not work against the managed database — and it\n * fails silently, which is worse than failing. The reason is specific and\n * measurable:\n *\n * PGlite is a *single* backend session, and `PGLiteSocketServer` multiplexes\n * every client connection onto it. `LISTEN` is therefore session-wide: whichever\n * client issues it arms the whole database. But a `NotificationResponse` is an\n * asynchronous message with no request to answer, so the multiplexer hands it to\n * whichever socket happens to be reading the protocol stream at that moment —\n * which is the client that *caused* the notification, not the one that asked for\n * it.\n *\n * Measured against pglite-socket 0.2.9:\n *\n * LISTEN and NOTIFY on one connection → delivered\n * trigger-fired pg_notify, same connection → delivered\n * another connection causes the notify → NOT delivered to the listener\n * …and the same notification IS delivered to the notifier, which never asked\n *\n * The realtime engine listens on a dedicated connection and the writes come\n * from request connections, so it is exactly the broken case, every time.\n *\n * The fix is to stop treating a notification as belonging to one connection,\n * which for a single-session database is the truth anyway: this proxy watches\n * the server→client direction, and every `NotificationResponse` frame it sees is\n * copied to every other connected client. A client that never issued `LISTEN`\n * may receive one it did not ask for; `pg` raises a `notification` event nobody\n * has subscribed to, which costs nothing. A client that *did* ask now always\n * gets it, which is the whole point.\n *\n * Two properties make this safe rather than clever:\n *\n * - **It never parses SQL and never rewrites a byte.** Frames are forwarded\n * verbatim; the only edit is delivering a copy of one to more sockets.\n * - **Injection only happens on a message boundary.** The server→client stream\n * is reassembled into whole protocol messages before anything is written on,\n * so an injected frame can never land inside another message.\n *\n * This exists only for the managed development database. Against a real Postgres\n * there is no proxy, because there is no defect to correct.\n */\n\nimport net from \"net\";\n\n/** `NotificationResponse`. The one message type this proxy treats specially. */\nconst NOTIFICATION_RESPONSE = 0x41; // 'A'\n\n/**\n * The SSL negotiation request, which is the one thing on the wire that is not\n * a typed message.\n *\n * A client may open with an 8-byte `SSLRequest` (length 8, code 80877103), and\n * the server answers with a *single untyped byte* — `N` or `S`. Feeding that\n * byte to a parser expecting `type + Int32 length` would desynchronise the\n * stream for the rest of the connection, so it is recognised and passed through.\n */\nconst SSL_REQUEST_LENGTH = 8;\nconst SSL_REQUEST_CODE = 80877103;\n\nfunction isSslRequest(chunk: Buffer): boolean {\n return (\n chunk.length >= SSL_REQUEST_LENGTH &&\n chunk.readInt32BE(0) === SSL_REQUEST_LENGTH &&\n chunk.readInt32BE(4) === SSL_REQUEST_CODE\n );\n}\n\n/**\n * Reassembles a server→client byte stream into whole protocol messages.\n *\n * Every backend message is `Int8 type` + `Int32 length` + payload, where the\n * length counts itself but not the type byte. Anything shorter than a full\n * message is held until the rest arrives — TCP offers no guarantee that a\n * message arrives in one chunk, and a proxy that assumed otherwise would inject\n * into the middle of a row description under load.\n */\nexport class BackendMessageParser {\n private buffered: Buffer = Buffer.alloc(0);\n /** Set once the untyped SSL negotiation byte has been dealt with. */\n private awaitingSslReply = false;\n\n expectSslReply(): void {\n this.awaitingSslReply = true;\n }\n\n /** Feed bytes in; get whole messages out, in order. */\n push(chunk: Buffer): Buffer[] {\n const messages: Buffer[] = [];\n this.buffered = this.buffered.length === 0 ? chunk : Buffer.concat([this.buffered, chunk]);\n\n if (this.awaitingSslReply && this.buffered.length >= 1) {\n // Single untyped byte: 'N' (no SSL) or 'S' (proceed).\n messages.push(this.buffered.subarray(0, 1));\n this.buffered = this.buffered.subarray(1);\n this.awaitingSslReply = false;\n }\n\n while (this.buffered.length >= 5) {\n const length = this.buffered.readInt32BE(1);\n // A length below 4 cannot describe itself; the stream is not one we\n // understand, so stop parsing and let the rest through untouched\n // rather than guessing.\n if (length < 4) break;\n const total = length + 1;\n if (this.buffered.length < total) break;\n messages.push(this.buffered.subarray(0, total));\n this.buffered = this.buffered.subarray(total);\n }\n\n return messages;\n }\n\n /** Bytes held back because they are not yet a whole message. */\n get pending(): number {\n return this.buffered.length;\n }\n}\n\nexport function isNotificationFrame(message: Buffer): boolean {\n return message.length > 0 && message[0] === NOTIFICATION_RESPONSE;\n}\n\n/** Channel and payload of a NotificationResponse, for logging and tests. */\nexport function decodeNotification(message: Buffer): { channel: string; payload: string } | null {\n if (!isNotificationFrame(message) || message.length < 10) return null;\n // 1 type byte + 4 length + 4 process id, then two null-terminated strings.\n const body = message.subarray(9);\n const split = body.indexOf(0);\n if (split === -1) return null;\n const channel = body.subarray(0, split).toString(\"utf8\");\n const rest = body.subarray(split + 1);\n const end = rest.indexOf(0);\n\n return { channel, payload: (end === -1 ? rest : rest.subarray(0, end)).toString(\"utf8\") };\n}\n\nexport interface NotificationProxyOptions {\n /** Port clients connect to. */\n listenPort: number;\n /** Port the real PGlite socket server is on. */\n upstreamPort: number;\n host?: string;\n /** Called for every notification broadcast. For diagnostics and tests. */\n onNotification?: (channel: string, payload: string, copies: number) => void;\n}\n\ninterface Connection {\n client: net.Socket;\n upstream: net.Socket;\n parser: BackendMessageParser;\n}\n\n/**\n * The proxy itself.\n *\n * One upstream connection per client connection, so the multiplexer downstream\n * sees exactly what it would have seen without the proxy.\n */\nexport class NotificationProxy {\n private server: net.Server | null = null;\n private readonly connections = new Set<Connection>();\n\n constructor(private readonly options: NotificationProxyOptions) {}\n\n get connectionCount(): number {\n return this.connections.size;\n }\n\n start(): Promise<void> {\n const host = this.options.host ?? \"127.0.0.1\";\n\n return new Promise((resolve, reject) => {\n const server = net.createServer((client) => this.accept(client, host));\n server.once(\"error\", reject);\n server.listen(this.options.listenPort, host, () => {\n this.server = server;\n resolve();\n });\n });\n }\n\n private accept(client: net.Socket, host: string): void {\n const upstream = net.connect(this.options.upstreamPort, host);\n const connection: Connection = { client, upstream, parser: new BackendMessageParser() };\n this.connections.add(connection);\n\n // Nagle would batch a notification behind nothing at all, adding latency\n // to the one message whose entire value is arriving promptly.\n client.setNoDelay(true);\n upstream.setNoDelay(true);\n\n client.on(\"data\", (chunk: Buffer) => {\n if (isSslRequest(chunk)) connection.parser.expectSslReply();\n upstream.write(chunk);\n });\n\n upstream.on(\"data\", (chunk: Buffer) => {\n for (const message of connection.parser.push(chunk)) {\n client.write(message);\n if (isNotificationFrame(message)) this.broadcast(message, connection);\n }\n });\n\n const close = () => {\n this.connections.delete(connection);\n client.destroy();\n upstream.destroy();\n };\n client.on(\"close\", close);\n client.on(\"error\", close);\n upstream.on(\"close\", close);\n upstream.on(\"error\", close);\n }\n\n /**\n * Copy a notification to every other client.\n *\n * Written directly rather than through a parser: it is already a whole\n * message, and every other socket is only ever written whole messages, so\n * there is no boundary to land inside.\n */\n private broadcast(message: Buffer, origin: Connection): void {\n let copies = 0;\n for (const connection of this.connections) {\n if (connection === origin) continue;\n if (connection.client.destroyed || !connection.client.writable) continue;\n connection.client.write(message);\n copies += 1;\n }\n\n const decoded = decodeNotification(message);\n if (decoded) this.options.onNotification?.(decoded.channel, decoded.payload, copies);\n }\n\n async stop(): Promise<void> {\n for (const connection of [...this.connections]) {\n connection.client.destroy();\n connection.upstream.destroy();\n }\n this.connections.clear();\n\n const server = this.server;\n this.server = null;\n if (!server) return;\n\n await new Promise<void>((resolve) => server.close(() => resolve()));\n }\n}\n","/**\n * The managed database process: one PGlite instance behind a Postgres socket.\n *\n * Runs as `rebase __dev-db-daemon`, a hidden subcommand rather than a separate\n * build entry point, so the same resolution works from `src` under tsx and from\n * the bundled `dist` a published CLI ships — there is no second file for a\n * build config to forget.\n *\n * It is deliberately detached from whoever started it. `rebase db push` in one\n * terminal and `rebase dev` in another must reach the same database, because\n * two processes opening one PGlite data directory would corrupt it, so the\n * daemon belongs to the *project* rather than to a command. What starts it is\n * incidental; what stops it is an explicit `rebase db stop`, an idle timeout,\n * or the machine going away.\n *\n * PGlite is imported dynamically. It is an optional dependency carrying a 25MB\n * WASM build, and the cost of that must fall only on someone who actually uses\n * the managed database — never on `rebase init`, and never on a CLI startup\n * that is about to print help.\n */\n\nimport fs from \"fs\";\nimport net from \"net\";\n\nimport {\n MANAGED_SERVER_MAX_CONNECTIONS,\n PGLITE_EXTENSIONS\n} from \"./constraints\";\nimport { NotificationProxy } from \"./notification-proxy\";\nimport { clearState, dataDir, findFreePort, writeState } from \"./state\";\n\n/** Shut down after this long with nothing connected. */\nconst DEFAULT_IDLE_TIMEOUT_MS = 30 * 60_000;\n\n/** How often to check for idleness. */\nconst IDLE_CHECK_INTERVAL_MS = 60_000;\n\nexport interface DaemonArgs {\n projectRoot: string;\n port: number;\n token: string;\n idleTimeoutMs: number;\n}\n\n/**\n * `--project <dir> --port <n> --token <t> [--idle-timeout <ms>]`.\n *\n * Every field is required and unvalidated input is fatal: this process is\n * spawned by the CLI, never typed by a person, so a malformed argument is a bug\n * in the caller and guessing would hide it.\n */\nexport function parseDaemonArgs(argv: readonly string[]): DaemonArgs {\n const take = (flag: string): string | null => {\n const index = argv.indexOf(flag);\n\n return index >= 0 && index + 1 < argv.length ? argv[index + 1] : null;\n };\n\n const projectRoot = take(\"--project\");\n const port = Number(take(\"--port\"));\n const token = take(\"--token\");\n const idleRaw = take(\"--idle-timeout\");\n\n if (!projectRoot) throw new Error(\"__dev-db-daemon: --project is required\");\n if (!Number.isInteger(port) || port <= 0 || port > 65535) {\n throw new Error(\"__dev-db-daemon: --port must be a valid port\");\n }\n if (!token) throw new Error(\"__dev-db-daemon: --token is required\");\n\n const idleTimeoutMs = idleRaw === null ? DEFAULT_IDLE_TIMEOUT_MS : Number(idleRaw);\n if (!Number.isFinite(idleTimeoutMs) || idleTimeoutMs < 0) {\n throw new Error(\"__dev-db-daemon: --idle-timeout must be a non-negative number of milliseconds\");\n }\n\n return { projectRoot, port, token, idleTimeoutMs };\n}\n\n/**\n * Load the extension bundles PGlite needs.\n *\n * `CREATE EXTENSION pg_trgm` cannot install anything on its own here — PGlite\n * resolves extensions from bundles handed to the constructor, and a missing one\n * fails at migration time with `extension \"pg_trgm\" is not available`, which\n * reads like a broken database rather than a missing import.\n *\n * Every failure here is loud for that reason, including the one that is new:\n * pgvector arrives from a package of its own, so unlike the contrib bundles it\n * can be absent while PGlite itself is fine.\n */\nasync function loadExtensions(): Promise<Record<string, unknown>> {\n const extensions: Record<string, unknown> = {};\n for (const extension of PGLITE_EXTENSIONS) {\n let module: Record<string, unknown>;\n try {\n module = (await import(extension.module)) as Record<string, unknown>;\n } catch (err) {\n throw new Error(\n `Could not load \"${extension.module}\", which supplies the ${extension.name} extension: ` +\n `${err instanceof Error ? err.message : String(err)}\\n` +\n \"It is an optional dependency of @rebasepro/cli — reinstall the project, \" +\n \"or run `rebase dev --docker` to use a real Postgres instead.\"\n );\n }\n const bundle = module[extension.export];\n if (!bundle) {\n throw new Error(\n `${extension.module} did not export \"${extension.export}\". ` +\n \"The installed PGlite version may not ship this extension.\"\n );\n }\n extensions[extension.name] = bundle;\n }\n\n return extensions;\n}\n\n/**\n * A tiny sidecar listener that answers one question: \"are you the daemon this\n * state file describes?\"\n *\n * Liveness cannot be answered by the pid — after a reboot the number belongs to\n * something else — nor by the port alone, for the same reason. Both would let\n * Rebase send a migration to a stranger. So the daemon publishes a token on a\n * second loopback port and the answer is only yes when the token matches.\n */\nfunction startIdentityServer(token: string, onConnection: () => void): Promise<net.Server> {\n return new Promise((resolve, reject) => {\n const server = net.createServer((socket) => {\n onConnection();\n socket.end(`rebase-dev-db ${token}\\n`);\n });\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => resolve(server));\n });\n}\n\nexport async function runDaemon(args: DaemonArgs): Promise<void> {\n const directory = dataDir(args.projectRoot);\n fs.mkdirSync(directory, { recursive: true });\n\n const { PGlite } = (await import(\"@electric-sql/pglite\")) as {\n PGlite: { create(options: unknown): Promise<unknown> };\n };\n const { PGLiteSocketServer } = (await import(\"@electric-sql/pglite-socket\")) as {\n PGLiteSocketServer: new (options: unknown) => {\n start(): Promise<void>;\n stop(): Promise<void>;\n getStats(): { activeConnections: number; queuedQueries: number };\n };\n };\n\n const extensions = await loadExtensions();\n const db = (await PGlite.create({ dataDir: directory, extensions })) as { close(): Promise<void> };\n\n // The socket server listens privately; clients reach it through the\n // notification proxy on `args.port`. Realtime does not work otherwise —\n // PGlite is one session, so a NotificationResponse is handed to whichever\n // socket is reading rather than to the one that issued LISTEN. See\n // `notification-proxy.ts` for the measurements.\n const upstreamPort = await findFreePort();\n const server = new PGLiteSocketServer({\n db,\n port: upstreamPort,\n host: \"127.0.0.1\",\n // Above the client pool limit so a second *non-transactional* client is\n // refused with a connection error rather than deadlocking the\n // multiplexer. See `constraints.ts` — the pool limit is what actually\n // prevents overlapping transactions.\n maxConnections: MANAGED_SERVER_MAX_CONNECTIONS\n });\n await server.start();\n\n const proxy = new NotificationProxy({\n listenPort: args.port,\n upstreamPort,\n onNotification: (channel, _payload, copies) => {\n if (copies > 0) process.stdout.write(`dev-db: relayed notification on ${channel} to ${copies} client(s)\\n`);\n }\n });\n await proxy.start();\n\n // \"Idle\" means nothing is connected to the *database*. An earlier version\n // tracked identity pings instead, which meant a daemon serving queries\n // steadily for an hour would decide it was idle and shut down under a\n // running dev server.\n let idleSince: number | null = Date.now();\n const identity = await startIdentityServer(args.token, () => {\n idleSince = null;\n });\n const identityAddress = identity.address();\n const identityPort = identityAddress !== null && typeof identityAddress !== \"string\" ? identityAddress.port : 0;\n\n writeState(args.projectRoot, {\n port: args.port,\n pid: process.pid,\n dataDir: directory,\n startedAt: new Date().toISOString(),\n token: args.token,\n identityPort\n });\n\n let shuttingDown = false;\n const shutdown = async (reason: string) => {\n if (shuttingDown) return;\n shuttingDown = true;\n process.stdout.write(`dev-db: stopping (${reason})\\n`);\n // The state file goes first: a command that reads it during shutdown\n // should conclude \"not running\" and start a fresh daemon, rather than\n // connect to a socket that is closing under it.\n clearState(args.projectRoot);\n try {\n await proxy.stop();\n } catch { /* already down */ }\n try {\n await server.stop();\n } catch { /* already down */ }\n identity.close();\n try {\n await db.close();\n } catch { /* already closed */ }\n process.exit(0);\n };\n\n process.on(\"SIGINT\", () => void shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => void shutdown(\"SIGTERM\"));\n // The parent going away must not take the database with it — the daemon\n // belongs to the project. But an orphan with nobody left to serve should\n // not outlive the session either, which is what the idle timer is for.\n process.on(\"disconnect\", () => { /* detached on purpose */ });\n\n if (args.idleTimeoutMs > 0) {\n const timer = setInterval(() => {\n const stats = server.getStats();\n const busy = stats.activeConnections > 0 || stats.queuedQueries > 0;\n if (busy) {\n idleSince = null;\n\n return;\n }\n if (idleSince === null) {\n idleSince = Date.now();\n\n return;\n }\n if (Date.now() - idleSince >= args.idleTimeoutMs) {\n void shutdown(`idle for ${Math.round(args.idleTimeoutMs / 60_000)} minutes`);\n }\n }, IDLE_CHECK_INTERVAL_MS);\n timer.unref();\n }\n\n process.stdout.write(`dev-db: ready on 127.0.0.1:${args.port}\\n`);\n}\n"],"mappings":"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"}
|
|
@@ -53,14 +53,30 @@
|
|
|
53
53
|
* against the managed database, with no change to the server: it does
|
|
54
54
|
* ordinary `LISTEN` over ordinary libpq.
|
|
55
55
|
*/
|
|
56
|
+
/** One extension bundle, and where to import it from. */
|
|
57
|
+
export interface PgliteExtension {
|
|
58
|
+
/** The name `CREATE EXTENSION` uses, and the key PGlite is handed. */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Module the bundle is imported from. */
|
|
61
|
+
module: string;
|
|
62
|
+
/** The named export carrying the bundle. */
|
|
63
|
+
export: string;
|
|
64
|
+
}
|
|
56
65
|
/**
|
|
57
66
|
* Extensions to hand PGlite's constructor.
|
|
58
67
|
*
|
|
59
68
|
* `CREATE EXTENSION` alone cannot install these — PGlite resolves them from
|
|
60
69
|
* bundles supplied at construction time, so anything missing here is missing
|
|
61
70
|
* from the database no matter what the migration says.
|
|
71
|
+
*
|
|
72
|
+
* The module is spelled out per extension rather than derived from the name,
|
|
73
|
+
* because they do not all live in the same place: the two contrib ones are
|
|
74
|
+
* subpaths of PGlite itself, and pgvector is a package of its own. Deriving the
|
|
75
|
+
* path is what left `vector` off this list — `@electric-sql/pglite/contrib/vector`
|
|
76
|
+
* does not exist, so a project declaring a `{ type: "vector" }` property could
|
|
77
|
+
* not use the managed dev database at all.
|
|
62
78
|
*/
|
|
63
|
-
export declare const
|
|
79
|
+
export declare const PGLITE_EXTENSIONS: readonly PgliteExtension[];
|
|
64
80
|
/**
|
|
65
81
|
* Client connections the managed database tolerates: exactly one.
|
|
66
82
|
*
|