@rebasepro/cli 0.16.0 → 0.16.1-canary.g0d7af95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/bundle.d.ts +28 -2
  2. package/dist/commands/build.d.ts +10 -0
  3. package/dist/commands/cloud/context.d.ts +17 -1
  4. package/dist/commands/cloud/databases.d.ts +1 -0
  5. package/dist/commands/cloud/deploy.d.ts +58 -0
  6. package/dist/commands/cloud/deployments.d.ts +42 -0
  7. package/dist/commands/cloud/env.d.ts +1 -0
  8. package/dist/commands/cloud/extensions.d.ts +1 -0
  9. package/dist/commands/cloud/projects.d.ts +14 -4
  10. package/dist/commands/cloud/resources.d.ts +10 -1
  11. package/dist/commands/db.d.ts +16 -0
  12. package/dist/commands/dev.d.ts +11 -0
  13. package/dist/commands/doctor.d.ts +1 -1
  14. package/dist/commands/init.d.ts +1 -1
  15. package/dist/commands/resources.d.ts +1 -0
  16. package/dist/constraints-BK1_4vci.js +80 -0
  17. package/dist/constraints-BK1_4vci.js.map +1 -0
  18. package/dist/daemon-Bdl4lrdt.js +252 -0
  19. package/dist/daemon-Bdl4lrdt.js.map +1 -0
  20. package/dist/daemon-entry-Brq-S8XX.js +378 -0
  21. package/dist/daemon-entry-Brq-S8XX.js.map +1 -0
  22. package/dist/dev-db/__fixtures__/cli-entry.d.ts +1 -0
  23. package/dist/dev-db/constraints.d.ts +98 -0
  24. package/dist/dev-db/daemon-entry.d.ts +35 -0
  25. package/dist/dev-db/daemon.d.ts +92 -0
  26. package/dist/dev-db/notification-proxy.d.ts +102 -0
  27. package/dist/dev-db/prepare.d.ts +63 -0
  28. package/dist/dev-db/pull.d.ts +92 -0
  29. package/dist/dev-db/resolve.d.ts +66 -0
  30. package/dist/dev-db/state.d.ts +93 -0
  31. package/dist/function-portability.d.ts +45 -0
  32. package/dist/index.d.ts +17 -17
  33. package/dist/index.es.js +5638 -4098
  34. package/dist/index.es.js.map +1 -1
  35. package/dist/manifest.d.ts +24 -1
  36. package/dist/pull-DqPRu1te.js +167 -0
  37. package/dist/pull-DqPRu1te.js.map +1 -0
  38. package/dist/resources/derive.d.ts +47 -0
  39. package/dist/state-c0CJ6Kwb.js +190 -0
  40. package/dist/state-c0CJ6Kwb.js.map +1 -0
  41. package/dist/telemetry/consent.d.ts +1 -1
  42. package/dist/telemetry/index.d.ts +7 -7
  43. package/dist/utils/dev-preflight.d.ts +73 -0
  44. package/package.json +13 -8
  45. package/templates/eject/backend/src/index.ts +15 -8
  46. package/templates/eject/config/resources.ts +24 -0
  47. package/templates/template/AGENTS.md +1 -1
  48. package/templates/template/CLAUDE.md +1 -1
  49. package/templates/template/README.md +1 -1
  50. package/templates/template/ai-instructions.md +5 -2
  51. package/templates/template/backend/functions/hello.ts +43 -22
  52. package/templates/template/config/resources.ts +57 -0
  53. package/templates/template/docker-compose.yml +10 -1
  54. package/templates/template/gitignore +1 -0
package/dist/bundle.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type DeclaredStorageSources, type NativeDependency, type RebaseBundleManifest, type RebaseBackendAppConfig } from "@rebasepro/types";
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
- storage?: DeclaredStorageSources;
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
  *
@@ -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>;
@@ -139,6 +139,7 @@ export declare const GLOBAL_CLOUD_FLAGS: {
139
139
  readonly "--yes": BooleanConstructor;
140
140
  readonly "--help": BooleanConstructor;
141
141
  readonly "--project": StringConstructor;
142
+ readonly "--url": StringConstructor;
142
143
  readonly "-p": "--project";
143
144
  readonly "-y": "--yes";
144
145
  readonly "-h": "--help";
@@ -192,7 +193,22 @@ export declare function printJson(value: unknown): void;
192
193
  * (and nothing else); otherwise run `human`. Keeping the two behind a single
193
194
  * call is what guarantees a command can never print a table AND a JSON blob.
194
195
  */
195
- export declare function emit(human: () => void, json: unknown): void;
196
+ export declare function emit<T>(human: () => void, json: JsonArg<T>): void;
197
+ /**
198
+ * The JSON payload, which must be a value and not a producer of one.
199
+ *
200
+ * `json: unknown` accepted a function without complaint, and four call sites in
201
+ * `resources.ts` passed `() => ({ … })` — mirroring the human argument beside
202
+ * it, which *is* a thunk. In JSON mode `printJson` then stringified a function,
203
+ * which is `undefined`. So `rebase cloud resources`, `resources set` and two
204
+ * cluster commands printed the single word `undefined` on every piped or
205
+ * `--json` run — and since this family forces JSON mode off a TTY, that is
206
+ * every scripted use of them.
207
+ *
208
+ * Typed so the mistake cannot be made again: a function resolves the parameter
209
+ * to `never`, and the call site fails to compile.
210
+ */
211
+ export type JsonArg<T> = T extends (...args: never[]) => unknown ? never : T;
196
212
  /**
197
213
  * Print a help page — the human one, or a machine-readable description of the
198
214
  * same command in JSON mode.
@@ -30,6 +30,7 @@ export declare function resolveBackupArgs(rawArgs: string[]): {
30
30
  readonly "--yes": BooleanConstructor;
31
31
  readonly "--help": BooleanConstructor;
32
32
  readonly "--project": StringConstructor;
33
+ readonly "--url": StringConstructor;
33
34
  readonly "-p": "--project";
34
35
  readonly "-y": "--yes";
35
36
  readonly "-h": "--help";
@@ -1,3 +1,17 @@
1
+ /**
2
+ * `rebase cloud deploy` and `rebase cloud logs`.
3
+ *
4
+ * `deploy` triggers the control-plane `deploy` function, then tails the build
5
+ * logs from the deployment record until it succeeds or fails. `logs` shows the
6
+ * latest build log, or runtime logs with `--runtime`.
7
+ *
8
+ * There are three deploys behind the one verb, and which one runs depends on the
9
+ * flags: `--bundle` builds and uploads a managed bundle, `--source .` uploads
10
+ * this directory as a build context, and the bare form uploads nothing and asks
11
+ * the control plane to rebuild what it already holds. That last one is the
12
+ * dangerous one — see `planBareDeploy`.
13
+ */
14
+ import arg from "arg";
1
15
  /** A project row, reduced to what says how it deploys (camel or snake columns). */
2
16
  export interface DeployProjectRow {
3
17
  runtimeMode?: string;
@@ -110,5 +124,49 @@ export declare function ejectRefusal(opts: EjectContext, projectRef: string): {
110
124
  export declare function deployWarnings(opts: EjectContext, projectRef: string): DeployWarning[];
111
125
  /** The warning half of a deploy's JSON payload — merged into whatever it emits. */
112
126
  export declare function warningPayload(warnings: DeployWarning[]): Record<string, unknown>;
127
+ /**
128
+ * `rebase cloud deploy [app]` — its flags, and which app of this repository the
129
+ * line named.
130
+ *
131
+ * Parsed through `parseCloudArgs` rather than `arg` directly, and the reason is
132
+ * the positional. This command used to parse `rawArgs.slice(2)` permissively
133
+ * and read `_[0]` as the app name — but `rawArgs` is the WHOLE `process.argv`,
134
+ * so `_` opens with the command words themselves. `_[0]` was therefore the
135
+ * literal string `"cloud"` on every run, which then went to `selectDeployApp`
136
+ * and came back as:
137
+ *
138
+ * This repository declares no app named "cloud". It declares: backend, web.
139
+ *
140
+ * So the documented `rebase cloud deploy --bundle` failed on every project that
141
+ * did not happen to declare an app called `cloud`, `rebase cloud deploy web`
142
+ * could not reach `web`, and the refusal named the user's real apps — reading
143
+ * as a fault in their `rebase.json` rather than in the CLI's own parse.
144
+ *
145
+ * `commandWords` counts from `cloud` itself, so `cloud deploy` is 2, and it is
146
+ * applied to the PARSED positionals: a flag written before the group no longer
147
+ * shifts the app name either.
148
+ */
149
+ export declare function resolveDeployArgs(rawArgs: string[]): {
150
+ flags: arg.Result<{
151
+ "--no-follow": BooleanConstructor;
152
+ "--source": StringConstructor;
153
+ "--message": StringConstructor;
154
+ "--bundle": BooleanConstructor;
155
+ "--bundle-dir": StringConstructor;
156
+ "--skip-type-check": BooleanConstructor;
157
+ "--force": BooleanConstructor;
158
+ "-m": string;
159
+ } & {
160
+ readonly "--json": BooleanConstructor;
161
+ readonly "--yes": BooleanConstructor;
162
+ readonly "--help": BooleanConstructor;
163
+ readonly "--project": StringConstructor;
164
+ readonly "--url": StringConstructor;
165
+ readonly "-p": "--project";
166
+ readonly "-y": "--yes";
167
+ readonly "-h": "--help";
168
+ }>;
169
+ appName: string | undefined;
170
+ };
113
171
  export declare function deployCommand(rawArgs: string[], projectRef: string): Promise<void>;
114
172
  export declare function logsCommand(rawArgs: string[], projectRef: string): Promise<void>;
@@ -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
  *
@@ -66,6 +107,7 @@ export declare function resolveDeploymentIdArg(rawArgs: string[], command: strin
66
107
  readonly "--yes": BooleanConstructor;
67
108
  readonly "--help": BooleanConstructor;
68
109
  readonly "--project": StringConstructor;
110
+ readonly "--url": StringConstructor;
69
111
  readonly "-p": "--project";
70
112
  readonly "-y": "--yes";
71
113
  readonly "-h": "--help";
@@ -34,6 +34,7 @@ export declare function resolveEnvSetArgs(rawArgs: string[]): {
34
34
  readonly "--yes": BooleanConstructor;
35
35
  readonly "--help": BooleanConstructor;
36
36
  readonly "--project": StringConstructor;
37
+ readonly "--url": StringConstructor;
37
38
  readonly "-p": "--project";
38
39
  readonly "-y": "--yes";
39
40
  readonly "-h": "--help";
@@ -15,6 +15,7 @@ export declare function resolveExtensionArgs(rawArgs: string[], action: "enable"
15
15
  readonly "--yes": BooleanConstructor;
16
16
  readonly "--help": BooleanConstructor;
17
17
  readonly "--project": StringConstructor;
18
+ readonly "--url": StringConstructor;
18
19
  readonly "-p": "--project";
19
20
  readonly "-y": "--yes";
20
21
  readonly "-h": "--help";
@@ -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` is half the Stripe compute lookup key (`compute_<provider>_<vmSize>`),
13
- * so that is a mispricing, not a cosmetic slip.
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
  };
@@ -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>;
@@ -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>;
@@ -1,4 +1,4 @@
1
- import { type LibpqUrlFinding } from "../utils/libpq-url";
1
+ import { type LibpqUrlFinding } from "../utils/libpq-url.js";
2
2
  /**
3
3
  * Find connection strings libpq cannot parse, anywhere in the project.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { PackageManager, PMCommands } from "../utils/package-manager";
1
+ import type { PackageManager, PMCommands } from "../utils/package-manager.js";
2
2
  /**
3
3
  * Every scaffolded file that carries a `{{PLACEHOLDER}}`.
4
4
  *
@@ -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"}