create-rindle 0.7.9 → 0.7.11
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/README.md +2 -1
- package/lib/scaffold.mjs +1 -0
- package/package.json +1 -1
- package/templates/minimal/AGENTS.md +6 -1
- package/templates/minimal/README.md +4 -1
- package/templates/minimal/package.json +1 -0
- package/templates/minimal/rindle.ncl +1 -1
- package/templates/minimal/src/rindle-client.ts +2 -3
- package/templates/minimal/src/rindle-tanstack.ts +20 -0
- package/templates/minimal/src/routes/__root.tsx +9 -24
- package/templates/minimal/src/routes/index.tsx +4 -10
- package/templates/minimal/src/routes/r.$id.tsx +4 -8
- package/templates/minimal/src/ssr.ts +7 -6
- package/templates/minimal/src/RindleApp.tsx +0 -19
package/README.md
CHANGED
|
@@ -46,7 +46,8 @@ The generated routes use the current co-located fragment pattern:
|
|
|
46
46
|
|
|
47
47
|
- `src/components/*.queries.ts` defines each component's `defineFragment` beside the named
|
|
48
48
|
`defineQuery` that roots it.
|
|
49
|
-
- The home route
|
|
49
|
+
- The home route declares `roomsQuery()` through `@rindle/tanstack`'s `rindle.loader`, which seeds
|
|
50
|
+
SSR and waits for the same live query on client navigation, then calls
|
|
50
51
|
`useRoot(roomsQuery, RoomCardFragment)` to receive opaque room refs.
|
|
51
52
|
- Row components call `useFragment(RoomCardFragment, room)` to open narrow local reads without a
|
|
52
53
|
new server subscription.
|
package/lib/scaffold.mjs
CHANGED
|
@@ -84,6 +84,7 @@ export default defineConfig({
|
|
|
84
84
|
{ find: /^rindle-wasm-bin/, replacement: wasmBin },
|
|
85
85
|
{ find: /^@rindle\\/client$/, replacement: src("client") },
|
|
86
86
|
{ find: /^@rindle\\/react$/, replacement: src("react") },
|
|
87
|
+
{ find: /^@rindle\\/tanstack$/, replacement: src("tanstack") },
|
|
87
88
|
{ find: /^@rindle\\/optimistic$/, replacement: src("optimistic") },
|
|
88
89
|
{ find: /^@rindle\\/normalized$/, replacement: src("normalized") },
|
|
89
90
|
{ find: /^@rindle\\/remote$/, replacement: src("remote") },
|
package/package.json
CHANGED
|
@@ -67,6 +67,10 @@ review:
|
|
|
67
67
|
exact as rows enter and leave.
|
|
68
68
|
7. **Keep `*.queries.ts` modules React-free** — no `.tsx` imports. The browser,
|
|
69
69
|
the API authority, and the SSR loader all import these same modules.
|
|
70
|
+
8. **Declare route reads through `rindle.loader(...)`** (`src/rindle-tanstack.ts`).
|
|
71
|
+
It owns server preloading, client navigation readiness/cancellation, and
|
|
72
|
+
blocking stale reloads. Keep `rindle.Provider` at the root; do not recreate
|
|
73
|
+
the old `useMatches()` merge or a separate `<RindleSSR>` wrapper.
|
|
70
74
|
|
|
71
75
|
## File map
|
|
72
76
|
|
|
@@ -77,6 +81,7 @@ review:
|
|
|
77
81
|
| `shared/app-def.ts` | the shared contract: relationships, query builder, isomorphic mutators |
|
|
78
82
|
| `src/components/*.queries.ts` | named queries + fragments, co-located with their components |
|
|
79
83
|
| `src/rindle-client.ts` | the one-call browser wire-up (`createRindleClient`) |
|
|
84
|
+
| `src/rindle-tanstack.ts` | the shared `rindle.loader` + `rindle.Provider` integration |
|
|
80
85
|
| `server/app-api.ts` | the authority: `registerQueries` + `sharedApiMutators` + server-only policy |
|
|
81
86
|
| `src/routes/api.rindle.*.tsx` | TanStack Start server routes exposing the authority over HTTP |
|
|
82
87
|
| `rindle.ncl` | the one topology (the colocated pair) — `rindle up` runs it locally, `rindle deploy` provisions it |
|
|
@@ -86,4 +91,4 @@ review:
|
|
|
86
91
|
|
|
87
92
|
Per-page markdown mirrors live at `https://rindle.sh/docs/<slug>.md`. Most
|
|
88
93
|
relevant here: `synced-app-quickstart`, `client`, `api-server`, `schema`,
|
|
89
|
-
`fragments`, `supported-queries-ts`, `change-model`.
|
|
94
|
+
`fragments`, `ssr`, `supported-queries-ts`, `change-model`.
|
|
@@ -10,7 +10,9 @@ Three tiers, same as the Rindle flagship examples:
|
|
|
10
10
|
|
|
11
11
|
- **Browser** — a TanStack Start SPA whose data layer *is* Rindle. The wasm IVM engine runs
|
|
12
12
|
in-process: reads resolve locally and instantly, writes apply optimistically and reconcile on
|
|
13
|
-
confirmation.
|
|
13
|
+
confirmation. `@rindle/tanstack` binds route preloading, client navigation readiness, and the
|
|
14
|
+
SSR-to-live provider handoff. Views are co-located Relay-style fragments
|
|
15
|
+
(`src/components/*.queries.ts`).
|
|
14
16
|
- **API authority** (`server/app-api.ts`) — resolves named queries to ASTs, drives the **same
|
|
15
17
|
isomorphic mutators** the browser predicted (their logical ops rendered to SQL), and enforces
|
|
16
18
|
policy: reads are public, writes require an identity, and a `"spam"` name/body
|
|
@@ -104,6 +106,7 @@ of `vite build` and never ship to production.
|
|
|
104
106
|
| `server/app-api.ts` | the authority: query resolution, `sharedApiMutators`, policy (host-agnostic) |
|
|
105
107
|
| `server/rindle-http.ts` | adapts the authority to a Web Request (the Start API routes call it) |
|
|
106
108
|
| `src/routes/api.rindle.*.tsx` | the three API endpoints as Start server routes (the browser's API) |
|
|
109
|
+
| `src/rindle-tanstack.ts` | one route-loader/provider binding for SSR and client navigation |
|
|
107
110
|
| `src/ssr.ts` | first-paint preload — calls the authority in-process |
|
|
108
111
|
| `src/components/*.queries.ts` | co-located queries + fragments |
|
|
109
112
|
| `src/routes/*` | TanStack routes |
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
# backed up; scales 1→N in place); `followers > 1` maps to `read-scaled`. `localRetention = true`
|
|
17
17
|
# maps to `colocated` (one box, both processes, no streaming backup).
|
|
18
18
|
#
|
|
19
|
-
# Every follower count renders a local fleet edge
|
|
19
|
+
# Every follower count renders a local fleet edge;
|
|
20
20
|
# FOLLOWER-AFFINITY-DESIGN.md §10). `rindle dev` injects the server's unified connection from this
|
|
21
21
|
# file; browser query leases carry the public ws endpoint + placement ticket. Changing
|
|
22
22
|
# `followers = 1` to a wider fleet therefore requires no application configuration change.
|
|
@@ -47,9 +47,8 @@ export function onRejection(handler: RejectionHandler): () => void {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/** The live optimistic client — assigned once {@link bootClient} resolves.
|
|
51
|
-
*
|
|
52
|
-
* src/RindleApp.tsx gates the whole tree on it). */
|
|
50
|
+
/** The live optimistic client — assigned once {@link bootClient} resolves. `rindle.Provider` and all
|
|
51
|
+
* route loaders share this boot; components use the live binding for `app.mutate.*` event handlers. */
|
|
53
52
|
export let app: RindleApp;
|
|
54
53
|
|
|
55
54
|
/** Dynamically imports the wasm engine + optimistic glue (so the SSR/prerender shell never evaluates
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The one TanStack Router/Start binding for this app. Routes declare their Rindle queries through
|
|
2
|
+
// `rindle.loader(...)`; the root mounts `rindle.Provider` to merge SSR slices and hand the seeded
|
|
3
|
+
// store off to this same live browser client after hydration.
|
|
4
|
+
|
|
5
|
+
import { createRindleTanStack } from "@rindle/tanstack";
|
|
6
|
+
|
|
7
|
+
import { schema } from "../shared/app-def.ts";
|
|
8
|
+
import { bootClient } from "./rindle-client.ts";
|
|
9
|
+
|
|
10
|
+
export const rindle = createRindleTanStack({
|
|
11
|
+
schema,
|
|
12
|
+
boot: bootClient,
|
|
13
|
+
preload: async (queries) => {
|
|
14
|
+
// The adapter only calls preload on the server. Keep the static Vite guard as well so ssr.ts —
|
|
15
|
+
// which builds the daemon client and reads server env — is eliminated from the browser build.
|
|
16
|
+
if (!import.meta.env.SSR) return {};
|
|
17
|
+
const { preloadRindle } = await import("./ssr.ts");
|
|
18
|
+
return preloadRindle([...queries]);
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
// The root route: the HTML document + the app frame. Inside the document,
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// persistent chrome around the matched view (`children`).
|
|
1
|
+
// The root route: the HTML document + the app frame. Inside the document, rindle.Provider merges
|
|
2
|
+
// every matched route's SSR seed, renders it on the server AND through hydration, then boots the
|
|
3
|
+
// in-browser wasm engine and swaps to the live store. <TopBar> + <Toaster> are the persistent chrome.
|
|
5
4
|
//
|
|
6
|
-
// Each
|
|
7
|
-
//
|
|
5
|
+
// Each leaf route declares its first-paint/navigation query through rindle.loader(...); the adapter
|
|
6
|
+
// owns server preloading, client readiness, cancellation, and the SSR→SPA handoff.
|
|
8
7
|
|
|
9
|
-
import {
|
|
10
|
-
import { HeadContent, Outlet, Scripts, createRootRoute, useMatches } from "@tanstack/react-router";
|
|
11
|
-
import type { DehydratedState } from "@rindle/client";
|
|
8
|
+
import { HeadContent, Outlet, Scripts, createRootRoute } from "@tanstack/react-router";
|
|
12
9
|
|
|
13
|
-
import { RindleApp } from "../RindleApp.tsx";
|
|
14
10
|
import { TopBar } from "../components/TopBar.tsx";
|
|
15
11
|
import { Toaster } from "../components/Toaster.tsx";
|
|
16
12
|
import { DevTools } from "../devtools.tsx";
|
|
13
|
+
import { rindle } from "../rindle-tanstack.ts";
|
|
17
14
|
import appCss from "../styles.css?url";
|
|
18
15
|
|
|
19
16
|
export const Route = createRootRoute({
|
|
@@ -39,31 +36,19 @@ export const Route = createRootRoute({
|
|
|
39
36
|
});
|
|
40
37
|
|
|
41
38
|
function RootDocument() {
|
|
42
|
-
// Merge the dehydrated first-paint cache from EVERY matched route, so a first visit to any route
|
|
43
|
-
// seeds exactly the queries it renders.
|
|
44
|
-
const matches = useMatches();
|
|
45
|
-
const ssrState = useMemo<DehydratedState>(() => {
|
|
46
|
-
const merged: DehydratedState = {};
|
|
47
|
-
for (const match of matches) {
|
|
48
|
-
const slice = (match.loaderData as { rindle?: DehydratedState } | undefined)?.rindle;
|
|
49
|
-
if (slice) Object.assign(merged, slice);
|
|
50
|
-
}
|
|
51
|
-
return merged;
|
|
52
|
-
}, [matches]);
|
|
53
|
-
|
|
54
39
|
return (
|
|
55
40
|
<html lang="en">
|
|
56
41
|
<head>
|
|
57
42
|
<HeadContent />
|
|
58
43
|
</head>
|
|
59
44
|
<body>
|
|
60
|
-
<
|
|
45
|
+
<rindle.Provider>
|
|
61
46
|
<TopBar />
|
|
62
47
|
<main className="app-main">
|
|
63
48
|
<Outlet />
|
|
64
49
|
</main>
|
|
65
50
|
<Toaster />
|
|
66
|
-
</
|
|
51
|
+
</rindle.Provider>
|
|
67
52
|
{/* Dev-only floating devtools pane (tree-shaken out of production builds). */}
|
|
68
53
|
<DevTools />
|
|
69
54
|
<Scripts />
|
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
// The home view (`/`): the list of rooms, each with a LIVE message count, plus a form to create a
|
|
2
|
-
// room. Its loader seeds the rooms query for first paint
|
|
3
|
-
// the live
|
|
2
|
+
// room. Its Rindle loader seeds the rooms query for first paint and blocks client navigation until
|
|
3
|
+
// the live query is ready — post a message in any room and its count here updates with no polling.
|
|
4
4
|
|
|
5
5
|
import { createFileRoute } from "@tanstack/react-router";
|
|
6
6
|
import { fragmentKey, useRoot } from "@rindle/react";
|
|
7
|
-
import type { DehydratedState } from "@rindle/client";
|
|
8
7
|
|
|
9
8
|
import { RoomCardFragment, roomsQuery } from "../components/RoomCard.queries.ts";
|
|
10
9
|
import { RoomCard } from "../components/RoomCard.tsx";
|
|
11
10
|
import { NewRoomForm } from "../components/NewRoomForm.tsx";
|
|
11
|
+
import { rindle } from "../rindle-tanstack.ts";
|
|
12
12
|
|
|
13
13
|
export const Route = createFileRoute("/")({
|
|
14
|
-
loader:
|
|
15
|
-
if (!import.meta.env.SSR) return { rindle: {} };
|
|
16
|
-
// Dynamic import: ssr.ts is server-only (it builds the daemon client), so it must never enter the
|
|
17
|
-
// client bundle. The static `import.meta.env.SSR` guard + this dynamic import keep it out.
|
|
18
|
-
const { preloadRindle } = await import("../ssr.ts");
|
|
19
|
-
return { rindle: await preloadRindle([roomsQuery()]) };
|
|
20
|
-
},
|
|
14
|
+
loader: rindle.loader({ query: () => roomsQuery() }),
|
|
21
15
|
component: Home,
|
|
22
16
|
});
|
|
23
17
|
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
// A room view (`/r/:id`): the room's messages oldest-first, each a live row, plus the composer. The
|
|
2
|
-
//
|
|
3
|
-
// the live read and every new message streams in.
|
|
2
|
+
// Rindle loader seeds the detail query for first paint and blocks client navigation until it is
|
|
3
|
+
// ready; after hydration the wasm engine owns the live read and every new message streams in.
|
|
4
4
|
|
|
5
5
|
import { Link, createFileRoute } from "@tanstack/react-router";
|
|
6
6
|
import { fragmentKey, useRoot } from "@rindle/react";
|
|
7
|
-
import type { DehydratedState } from "@rindle/client";
|
|
8
7
|
|
|
9
8
|
import { roomDetailQuery } from "../components/RoomView.queries.ts";
|
|
10
9
|
import { MessageCard } from "../components/MessageCard.tsx";
|
|
11
10
|
import { Composer } from "../components/Composer.tsx";
|
|
11
|
+
import { rindle } from "../rindle-tanstack.ts";
|
|
12
12
|
|
|
13
13
|
export const Route = createFileRoute("/r/$id")({
|
|
14
|
-
loader:
|
|
15
|
-
if (!import.meta.env.SSR) return { rindle: {} };
|
|
16
|
-
const { preloadRindle } = await import("../ssr.ts");
|
|
17
|
-
return { rindle: await preloadRindle([roomDetailQuery(params.id)]) };
|
|
18
|
-
},
|
|
14
|
+
loader: rindle.loader({ query: ({ params }) => roomDetailQuery(params.id) }),
|
|
19
15
|
component: RoomView,
|
|
20
16
|
});
|
|
21
17
|
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
// (server/app-api.ts) — the SAME `createAppApi` factory the /api/rindle server route uses, called
|
|
4
4
|
// IN-PROCESS (no HTTP hop) because SSR and the API now share one server. The authority resolves
|
|
5
5
|
// `(name, args)` → AST itself; the loader never ships a raw AST. It returns the dehydrated snapshot
|
|
6
|
-
// for the HTML,
|
|
7
|
-
//
|
|
6
|
+
// for the HTML, `rindle.Provider` hydrates it for an instant correct first paint, then the wasm engine
|
|
7
|
+
// boots and the live `subscribe` reconciles.
|
|
8
8
|
//
|
|
9
9
|
// This is a STRICTLY server-side module: it builds the daemon client and reads `process.env`, so it is
|
|
10
|
-
// imported ONLY
|
|
11
|
-
//
|
|
10
|
+
// imported ONLY by src/rindle-tanstack.ts behind an `import.meta.env.SSR` guard, keeping it out of the
|
|
11
|
+
// client bundle. It never imports the engine.
|
|
12
12
|
|
|
13
13
|
import { createServerStore, type DehydratedState, type OneShotQueryFn, type OneShotResult, type Query } from "@rindle/client";
|
|
14
14
|
import type { ApiContext } from "@rindle/api-server";
|
|
@@ -32,8 +32,9 @@ const readInProcess: OneShotQueryFn = async ({ name, args }): Promise<OneShotRes
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
/** Preload the given NAMED queries through the authority and return the dehydrated first-paint cache to
|
|
35
|
-
* embed in the HTML.
|
|
36
|
-
* seed for that query (the live engine fills it in after hydration) rather than
|
|
35
|
+
* embed in the HTML. Called by the integration's server-only preload hook. `preloadAll` degrades a
|
|
36
|
+
* failed read to no seed for that query (the live engine fills it in after hydration) rather than
|
|
37
|
+
* breaking the whole page. */
|
|
37
38
|
export async function preloadRindle(queries: Array<Query<any, any, any>>): Promise<DehydratedState> {
|
|
38
39
|
return createServerStore(schema, { query: readInProcess }).preloadAll(queries, {
|
|
39
40
|
onError: (_query, err) =>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// The SSR→SPA store handoff. The handoff itself — seed the render from the SSR snapshot, boot the
|
|
2
|
-
// wasm engine after hydration, swap the store under `useQuery` with no flash — lives in `<RindleSSR>`
|
|
3
|
-
// (@rindle/react). This file just binds THIS app's `schema` + `bootClient` into it, so the route
|
|
4
|
-
// tree keeps rendering `<RindleApp ssrState={…}>` unchanged.
|
|
5
|
-
|
|
6
|
-
import type { ReactNode } from "react";
|
|
7
|
-
import { RindleSSR } from "@rindle/react";
|
|
8
|
-
import type { DehydratedState } from "@rindle/client";
|
|
9
|
-
|
|
10
|
-
import { schema } from "../shared/app-def.ts";
|
|
11
|
-
import { bootClient } from "./rindle-client.ts";
|
|
12
|
-
|
|
13
|
-
export function RindleApp({ ssrState, children }: { ssrState: DehydratedState; children: ReactNode }) {
|
|
14
|
-
return (
|
|
15
|
-
<RindleSSR schema={schema} ssrState={ssrState} boot={bootClient}>
|
|
16
|
-
{children}
|
|
17
|
-
</RindleSSR>
|
|
18
|
-
);
|
|
19
|
-
}
|