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 +1 -1
- package/templates/minimal/AGENTS.md +11 -13
- package/templates/minimal/README.md +20 -19
- package/templates/minimal/package.json +2 -6
- package/templates/minimal/rindle.ncl +6 -5
- package/templates/minimal/server/app-api.ts +19 -44
- package/templates/minimal/server/rindle-http.ts +6 -7
- package/templates/minimal/src/rindle-client.ts +5 -8
- package/templates/minimal/src/ssr.ts +2 -6
- package/templates/minimal/vite-env.d.ts +0 -11
- package/templates/minimal/vite.config.ts +1 -1
package/package.json
CHANGED
|
@@ -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: `
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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. **
|
|
64
|
-
|
|
65
|
-
|
|
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`
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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 —
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
|
8
|
-
# package.json `dev`
|
|
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
|
|
21
|
-
# file
|
|
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
|
|
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
|
-
/**
|
|
110
|
+
/** The one application-facing Rindle connection. */
|
|
115
111
|
export interface AppApiOptions {
|
|
116
|
-
/** The
|
|
117
|
-
|
|
118
|
-
/** The
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
|
|
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
|
-
|
|
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
|
|
155
|
-
export function
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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,
|
|
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
|
|
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> = {
|
|
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> => ({
|
|
72
|
+
headers: (): Record<string, string> => ({
|
|
73
|
+
"x-rindle-user": currentHandle(),
|
|
74
|
+
}),
|
|
78
75
|
},
|
|
79
|
-
//
|
|
80
|
-
|
|
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,
|
|
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
|
|
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
|
|
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)
|