create-rindle 0.7.2 → 0.7.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-rindle",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,17 +14,14 @@ Rindle docs are served as raw markdown for LLMs: index at
14
14
 
15
15
  ## Commands
16
16
 
17
- - `pnpm dev` — the one command: `concurrently` runs two processes — **fleet**
18
- (`rindle up` renders `rindle.ncl` and supervises the pair — the
19
- `rindle-replicator` write-master + the `rindled` follower, prebuilt binaries
20
- from `@rindle/cli`, no Rust toolchain) applies `migrations/*.sql` to the master
21
- and regenerates `shared/schema.gen.ts` from the follower (re-running both on
22
- every `migrations/` change), and **web** (`rindle exec -- vite dev` on :3000) receives bindings
23
- derived from `rindle.ncl` and serves the app +
24
- the `/api/rindle/*` server routes. Run either alone with `pnpm fleet` /
25
- `pnpm dev:web`.
17
+ - `pnpm dev` — the one lifecycle command: `rindle dev` evaluates `rindle.ncl`
18
+ once, supervises the `rindle-replicator` write-master + `rindled` follower +
19
+ fleet edge (prebuilt binaries from `@rindle/cli`, no Rust toolchain), waits for
20
+ them, applies `migrations/*.sql`, regenerates `shared/schema.gen.ts`, then runs
21
+ Vite on :3000 with `RINDLE_URL` + `RINDLE_DATABASE_TOKEN`. It watches migration
22
+ and follower-schema changes and tears down the whole process tree together.
26
23
  - `pnpm typecheck` — regenerates the route tree, then `tsc --noEmit`.
27
- - `pnpm migrate` — one-shot `rindle exec -- rindle migrate apply` against the write-master derived
24
+ - `pnpm migrate` — one-shot `rindle migrate apply` against the unified ingress derived
28
25
  from `rindle.ncl` (the follower's `/migrate` is write-fenced). The dev loop already
29
26
  applies on boot + on every `migrations/` change.
30
27
  - `pnpm rindle:deploy` / `pnpm rindle:migrate:remote` — deploy the data tier to
@@ -60,9 +57,10 @@ review:
60
57
  `src/components/*.queries.ts` and register them in `server/app-api.ts`. An
61
58
  ad-hoc `store.query.…` builder resolves **locally only** — it never opens a
62
59
  server subscription.
63
- 5. **Daemon tokens are server-only.** They gate the follower and write-master control planes and
64
- must never reach the browser; the browser reaches only the lease-gated fleet edge. Local endpoint
65
- bindings come from `rindle exec`; never copy topology ports into package scripts.
60
+ 5. **Database tokens are server-only.** `rindle dev` injects the one application connection as
61
+ `RINDLE_URL` + `RINDLE_DATABASE_TOKEN`; the bearer must never reach the browser. The browser
62
+ learns only the public WebSocket endpoint + placement ticket from its query-lease response.
63
+ Never copy topology ports into package scripts or add a browser config endpoint.
66
64
  6. **Subscribe to windows, not whole tables** — order + `limit`, and ratchet
67
65
  `limit` up for "load more". The engine keeps the window (and any `countAs`)
68
66
  exact as rows enter and leave.
@@ -38,21 +38,21 @@ pnpm install
38
38
  pnpm dev
39
39
  ```
40
40
 
41
- `pnpm dev` runs two processes with [`concurrently`](https://www.npmjs.com/package/concurrently) —
42
- the standard TanStack Start dev shape:
43
-
44
- - **`fleet`** `rindle up` renders `rindle.ncl` and supervises the whole pair (the
45
- `rindle-replicator` write-master + the `rindled` follower — prebuilt binaries from `@rindle/cli`,
46
- no Rust toolchain needed), applies the migrations to the master, and regenerates
47
- `shared/schema.gen.ts` from the follower's schema re-running both on every `migrations/*.sql`
48
- change (`--watch`).
49
- - **`web`** — `rindle exec -- vite dev` on <http://localhost:3000>, which serves both the app and the
50
- `/api/rindle/*` Start server routes. `rindle exec` derives the stable fleet-edge read/ws binding
51
- and write-master binding from `rindle.ncl`; no endpoint is copied into the package scripts.
52
-
53
- You can run either alone: `pnpm fleet` / `pnpm dev:web`. Both processes bind to loopback and run
54
- without an auth token in dev; a real deployment sets tokens (`RINDLE_DAEMON_TOKEN` for reads,
55
- `RINDLE_REPLICATOR_TOKEN` for writes on the server) that never reach the browser.
41
+ `pnpm dev` is one lifecycle command:
42
+
43
+ ```bash
44
+ rindle dev --migrate --gen shared/schema.gen.ts -- vite dev --port 3000
45
+ ```
46
+
47
+ It evaluates `rindle.ncl` once, supervises the write-master, follower, and stable fleet edge, waits
48
+ for the read path to be ready, applies migrations, regenerates the schema, then starts Vite with the
49
+ unified `RINDLE_URL` + `RINDLE_DATABASE_TOKEN` server bindings. Migration and follower-schema changes
50
+ are watched automatically; Ctrl-C or an app exit tears the whole process tree down. The browser has
51
+ no topology environment variable: its first query lease carries the public WebSocket endpoint and a
52
+ fresh placement ticket, so the optimistic client opens the correctly pinned connection lazily.
53
+
54
+ Normal development needs neither a second supervisor, a readiness probe, nor a topology-specific
55
+ web command.
56
56
 
57
57
  Open two browser windows to watch writes sync live: create a room, post a message, and watch the
58
58
  room's count update on the home page with no polling. Try a room name or message containing "spam" to
@@ -77,10 +77,11 @@ future deploys re-attach to the same app. `followers = 1` provisions a backed-up
77
77
  follower (`replicated`, scales 1→N in place); raise it for more read replicas (`read-scaled`);
78
78
  `localRetention = true` provisions the one-box colocated pair (`colocated`, no streaming backup —
79
79
  no durability beyond the box). That deploys the **data tier**; the
80
- **web app** (this Vite/TanStack Start app) deploys to any Node host — point its `RINDLE_DAEMON_URL`
81
- (fleet-edge reads) + `RINDLE_REPLICATOR_URL` (writes) + their tokens (server) at the deployed data
82
- tier, and set `VITE_FLEET_WS` to the stable public fleet ws. These bindings are identical for one or
83
- many followers, and follower affinity is always enabled.
80
+ **web app** (this Vite/TanStack Start app) deploys to any Node host — configure its server with the
81
+ one fleet ingress (`RINDLE_URL`) and SQL bearer (`RINDLE_DATABASE_TOKEN`). Query leases derive the
82
+ public WebSocket endpoint from that same URL, so there is no browser-side topology setting whether
83
+ the deployment has one follower or many. Set the server-only `RINDLE_WS_URL` override only when a
84
+ host exposes WebSocket ingress at a different origin.
84
85
 
85
86
  ## Devtools
86
87
 
@@ -8,13 +8,11 @@
8
8
  "node": ">=22.18"
9
9
  },
10
10
  "scripts": {
11
- "fleet": "rindle up --migrate --gen shared/schema.gen.ts --watch --dir migrations",
12
- "dev:web": "rindle exec -- vite dev --port 3000",
13
- "dev": "concurrently -k -n fleet,web -c blue,green \"pnpm fleet\" \"pnpm dev:web\"",
11
+ "dev": "rindle dev --migrate --gen shared/schema.gen.ts --dir migrations -- vite dev --port 3000",
14
12
  "generate-routes": "tsr generate",
15
13
  "build": "vite build",
16
14
  "preview": "vite preview",
17
- "migrate": "rindle exec -- rindle migrate apply --dir migrations",
15
+ "migrate": "rindle migrate apply --dir migrations",
18
16
  "rindle:deploy": "rindle deploy",
19
17
  "rindle:migrate:remote": "rindle migrate apply --remote",
20
18
  "typecheck": "tsr generate && tsc --noEmit"
@@ -22,7 +20,6 @@
22
20
  "dependencies": {
23
21
  "@rindle/api-server": "latest",
24
22
  "@rindle/client": "latest",
25
- "@rindle/daemon-client": "latest",
26
23
  "@rindle/optimistic": "latest",
27
24
  "@rindle/react": "latest",
28
25
  "@rindle/wasm": "latest",
@@ -39,7 +36,6 @@
39
36
  "@rindle/react-devtools": "latest",
40
37
  "@tanstack/router-cli": "^1.167.17",
41
38
  "@tanstack/router-plugin": "^1.168.18",
42
- "concurrently": "^10.0.3",
43
39
  "@types/node": "^22.10.0",
44
40
  "@types/react": "^19.0.0",
45
41
  "@types/react-dom": "^19.0.0",
@@ -2,10 +2,10 @@
2
2
  # write-master + one `rindled` read-follower. `followers = 1` is the colocated pair, the smallest
3
3
  # shape; raise it for more read replicas.
4
4
  #
5
- # A topology is a plain record of inputs; `rindle up` (local) and `rindle deploy` / `rindle render`
5
+ # A topology is a plain record of inputs; `rindle dev` (local) and `rindle deploy` / `rindle render`
6
6
  # (cloud) merge it onto the topology library embedded in the `rindle` binary — nothing to import
7
- # or install. LOCAL dev reads THIS file: `rindle up` renders it and supervises the pair (see
8
- # package.json `dev` / `fleet`). The same file drives the cloud:
7
+ # or install. LOCAL dev reads THIS file: `rindle dev` renders it and supervises the pair (see the
8
+ # package.json `dev` script). The same file drives the cloud:
9
9
  #
10
10
  # rindle login # once — authenticate to Rindle Cloud
11
11
  # rindle deploy # provision / re-attach the managed app (writes .rindle/cloud.json)
@@ -17,8 +17,9 @@
17
17
  # maps to `colocated` (one box, both processes, no streaming backup).
18
18
  #
19
19
  # Every follower count renders a local fleet edge (the Fly-edge stand-in;
20
- # FOLLOWER-AFFINITY-DESIGN.md §10). `rindle exec` derives the app's read/write/ws bindings from this
21
- # file, so changing `followers = 1` to a wider fleet requires no application configuration change.
20
+ # FOLLOWER-AFFINITY-DESIGN.md §10). `rindle dev` injects the server's unified connection from this
21
+ # file; browser query leases carry the public ws endpoint + placement ticket. Changing
22
+ # `followers = 1` to a wider fleet therefore requires no application configuration change.
22
23
  # Set `RINDLE_DEV_CLIENT_REGION=A` to simulate a client region; the edge always mints an affinity
23
24
  # placement ticket, with a fleet of one resolving trivially to its only follower.
24
25
  {
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // It is deliberately free of any host: server/rindle-http.ts adapts it to a Web Request for the Start
8
8
  // API routes (src/routes/api.rindle.*.tsx) the browser calls, and the SSR loader (src/ssr.ts) calls the
9
- // SAME factory in-process. The only per-host inputs are the daemon's control-plane URL + token and the
9
+ // SAME factory in-process. The only per-host inputs are the unified Rindle connection and the
10
10
  // AuthProvider.
11
11
 
12
12
  import {
@@ -16,7 +16,6 @@ import {
16
16
  runSharedMutation,
17
17
  scoped,
18
18
  sharedApiMutators,
19
- SplitDaemonClient,
20
19
  } from "@rindle/api-server";
21
20
  import type {
22
21
  ApiMutator,
@@ -26,9 +25,6 @@ import type {
26
25
  RindleApiServer,
27
26
  SharedMutatorWithArgs,
28
27
  } from "@rindle/api-server";
29
- import { HttpRindleDaemonClient } from "@rindle/daemon-client";
30
- import type { FetchLike } from "@rindle/daemon-client";
31
-
32
28
  import {
33
29
  mutators as sharedMutators,
34
30
  normalizeBody,
@@ -111,38 +107,21 @@ const apiMutators = defineApiMutators<User, ApiMutators<User>>({
111
107
  }),
112
108
  });
113
109
 
114
- /** A bearer-auth'd daemon control-plane target. */
110
+ /** The one application-facing Rindle connection. */
115
111
  export interface AppApiOptions {
116
- /** The follower/read daemon control-plane base URL bearer-auth'd. */
117
- daemonUrl: string;
118
- /** The shared bearer token for {@link daemonUrl} (daemon ↔ this tier ONLY — never reaches the browser). */
119
- daemonToken: string;
120
- /** The required write-master (`rindle-replicator`) control-plane base URL. Writes + mutation
121
- * sessions go here; reads stay on {@link daemonUrl}. */
122
- replicatorUrl: string;
123
- /** Bearer for {@link replicatorUrl}, if it gates its write plane. */
124
- replicatorToken?: string;
125
- /** Override the daemon HTTP transport (defaults to global `fetch`). */
126
- fetch?: FetchLike;
112
+ /** The unified fleet ingress for reads, SQL writes, migrations, and subscriptions. */
113
+ url: string;
114
+ /** The server-only public SQL bearer. Never returned to the browser. */
115
+ token: string;
116
+ /** Optional distinct public WebSocket ingress; normally derived from {@link url}. */
117
+ wsUrl?: string;
127
118
  }
128
119
 
129
120
  /** Build the configured API server. Stateless: safe to construct per-request or once per process.
130
121
  * Reads are PUBLIC; writes require a verified identity. */
131
122
  export function createAppApi(opts: AppApiOptions): RindleApiServer<User> {
132
- const reads = new HttpRindleDaemonClient({
133
- baseUrl: opts.daemonUrl,
134
- headers: { authorization: `Bearer ${opts.daemonToken}` },
135
- fetch: opts.fetch,
136
- });
137
123
  return createRindleApiServer<User>({
138
- daemon: new SplitDaemonClient(
139
- new HttpRindleDaemonClient({
140
- baseUrl: opts.replicatorUrl,
141
- headers: opts.replicatorToken ? { authorization: `Bearer ${opts.replicatorToken}` } : undefined,
142
- fetch: opts.fetch,
143
- }),
144
- reads,
145
- ),
124
+ rindle: { url: opts.url, token: opts.token, wsUrl: opts.wsUrl },
146
125
  schema, // drives the dialect-SQL renderer for the shared mutators' logical ops
147
126
  queries: apiQueries,
148
127
  mutators: apiMutators,
@@ -151,21 +130,17 @@ export function createAppApi(opts: AppApiOptions): RindleApiServer<User> {
151
130
  });
152
131
  }
153
132
 
154
- /** Resolve the daemon wiring from an environment bag, shared by both host shells. */
155
- export function resolveDaemon(
156
- env: Record<string, string | undefined>,
157
- defaults: { daemonUrl: string; daemonToken: string },
158
- ): Pick<AppApiOptions, "daemonUrl" | "daemonToken" | "replicatorUrl" | "replicatorToken"> {
159
- const replicatorUrl = env.RINDLE_REPLICATOR_URL;
160
- if (!replicatorUrl) {
161
- throw new Error("RINDLE_REPLICATOR_URL is required: writes must target the replicator write-master");
133
+ /** Resolve the one connection injected by `rindle dev` (or the production host). */
134
+ export function resolveRindle(env: Record<string, string | undefined>): AppApiOptions {
135
+ const url = env.RINDLE_URL;
136
+ const token = env.RINDLE_DATABASE_TOKEN;
137
+ if (!url || !token) {
138
+ throw new Error(
139
+ "RINDLE_URL + RINDLE_DATABASE_TOKEN are required — start the app with `rindle dev -- …`",
140
+ );
162
141
  }
163
- return {
164
- daemonUrl: env.RINDLE_DAEMON_URL ?? defaults.daemonUrl,
165
- daemonToken: env.RINDLE_DAEMON_TOKEN ?? defaults.daemonToken,
166
- replicatorUrl,
167
- replicatorToken: env.RINDLE_REPLICATOR_TOKEN,
168
- };
142
+ const wsUrl = env.RINDLE_WS_URL;
143
+ return { url, token, ...(wsUrl ? { wsUrl } : {}) };
169
144
  }
170
145
 
171
146
  /** Map an error thrown out of the API server (or body parsing) to an HTTP status + message. */
@@ -6,7 +6,7 @@
6
6
 
7
7
  import type { ApiContext } from "@rindle/api-server";
8
8
 
9
- import { createAppApi, httpErrorOf, resolveDaemon } from "./app-api.ts";
9
+ import { createAppApi, httpErrorOf, resolveRindle } from "./app-api.ts";
10
10
  import type { User } from "./app-api.ts";
11
11
  import { devAuth } from "./auth-dev.ts";
12
12
 
@@ -17,13 +17,12 @@ export type RindleRouteKind = "query" | "read" | "mutate";
17
17
  * built per request (cheap), so this is safe in a fresh-per-request serverless host too. */
18
18
  export async function handleRindleJson(kind: RindleRouteKind, request: Request): Promise<Response> {
19
19
  try {
20
- const daemon = resolveDaemon(process.env, {
21
- daemonUrl: "http://127.0.0.1:7600",
22
- daemonToken: "dev-daemon-token",
23
- });
24
- const api = createAppApi(daemon);
20
+ const api = createAppApi(resolveRindle(process.env));
25
21
  const body = await request.json().catch(() => ({}));
26
- const context: ApiContext<User> = { user: (await devAuth.verify(request)) ?? undefined, request };
22
+ const context: ApiContext<User> = {
23
+ user: (await devAuth.verify(request)) ?? undefined,
24
+ request,
25
+ };
27
26
  const out =
28
27
  kind === "query"
29
28
  ? await api.handleQueryJson(body, context)
@@ -60,11 +60,6 @@ async function bootClientInner() {
60
60
  import("@rindle/wasm"),
61
61
  ]);
62
62
  await initWasm(wasmUrl);
63
- // Every deployment exposes one stable fleet ws, including a fleet of one. `rindle exec` derives
64
- // VITE_FLEET_WS from rindle.ncl, so increasing `followers` never changes app configuration.
65
- // VITE_DAEMON_WS is a temporary direct-follower escape hatch for focused harnesses.
66
- const directWs = import.meta.env.VITE_DAEMON_WS?.trim();
67
- const fleetWs = import.meta.env.VITE_FLEET_WS?.trim() ?? "ws://127.0.0.1:7650";
68
63
  return createRindleClient({
69
64
  schema,
70
65
  mutators,
@@ -74,10 +69,12 @@ async function bootClientInner() {
74
69
  api: {
75
70
  url: "", // same-origin: /api/rindle/* is a Start server route on this same server
76
71
  // Identity per request: the dev handle header. A real app sends a verified token instead.
77
- headers: (): Record<string, string> => ({ "x-rindle-user": currentHandle() }),
72
+ headers: (): Record<string, string> => ({
73
+ "x-rindle-user": currentHandle(),
74
+ }),
78
75
  },
79
- // Normal app traffic always enters through the fleet edge. Direct follower access is explicit.
80
- daemon: directWs ? { wsUrl: directWs } : { wsUrl: fleetWs, affinity: true },
76
+ // No browser topology config: the first query lease returns the public WebSocket endpoint plus
77
+ // a fresh placement ticket, and the optimistic client opens the correctly pinned socket lazily.
81
78
  dev: { resetOnMutationGap: import.meta.env.DEV },
82
79
  onRejected: (envelope, reason) => rejectionHandler(envelope, reason),
83
80
  });
@@ -13,7 +13,7 @@
13
13
  import { createServerStore, type DehydratedState, type OneShotQueryFn, type OneShotResult, type Query } from "@rindle/client";
14
14
  import type { ApiContext } from "@rindle/api-server";
15
15
 
16
- import { createAppApi, resolveDaemon } from "../server/app-api.ts";
16
+ import { createAppApi, resolveRindle } from "../server/app-api.ts";
17
17
  import type { User } from "../server/app-api.ts";
18
18
  import { schema } from "../shared/app-def.ts";
19
19
 
@@ -26,11 +26,7 @@ const SSR_USER: User = undefined;
26
26
  * authority IN-PROCESS — the same path as the HTTP /api/rindle/read, minus the network round trip —
27
27
  * and hand back the assembled current view. */
28
28
  const readInProcess: OneShotQueryFn = async ({ name, args }): Promise<OneShotResult> => {
29
- const daemon = resolveDaemon(process.env, {
30
- daemonUrl: "http://127.0.0.1:7600",
31
- daemonToken: "dev-daemon-token",
32
- });
33
- const api = createAppApi(daemon);
29
+ const api = createAppApi(resolveRindle(process.env));
34
30
  const context: ApiContext<User> = { user: SSR_USER, request: undefined };
35
31
  return (await api.handleReadJson({ name, args }, context)) as OneShotResult;
36
32
  };
@@ -4,14 +4,3 @@ declare module "rindle-wasm-bin?url" {
4
4
  const url: string;
5
5
  export default url;
6
6
  }
7
-
8
- interface ImportMetaEnv {
9
- /** Deprecated direct-follower test/debug escape hatch. Normal apps use VITE_FLEET_WS. */
10
- readonly VITE_DAEMON_WS?: string;
11
- /** Stable fleet-edge PUBLIC ws, derived by `rindle exec` for one or many followers. */
12
- readonly VITE_FLEET_WS?: string;
13
- }
14
-
15
- interface ImportMeta {
16
- readonly env: ImportMetaEnv;
17
- }
@@ -27,7 +27,7 @@ export default defineConfig({
27
27
  // client-only dynamic import in src/rindle-client.ts, so it never loads in the shell pass).
28
28
  ssr: { noExternal: [/^@rindle\//] },
29
29
  // No `server.proxy`: /api/rindle/* are Start server routes (src/routes/api.rindle.*.tsx) served by
30
- // this same dev server. Browser subscriptions enter through the fleet edge derived by `rindle exec`.
30
+ // this same dev server. Browser subscriptions discover the fleet edge from their query lease.
31
31
  plugins: [
32
32
  // If you add TanStack's devtools panel, DISABLE console piping. The piping cross-forwards console
33
33
  // between the SSR server and the browser; a repeated log (a React warning, a Rindle fetch-retry)