create-rindle 0.7.10 → 0.7.12

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 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 preloads `roomsQuery()` for SSR, then calls
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-rindle",
3
- "version": "0.7.10",
3
+ "version": "0.7.12",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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. Views are co-located Relay-style fragments (`src/components/*.queries.ts`).
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 |
@@ -22,6 +22,7 @@
22
22
  "@rindle/client": "latest",
23
23
  "@rindle/optimistic": "latest",
24
24
  "@rindle/react": "latest",
25
+ "@rindle/tanstack": "latest",
25
26
  "@rindle/wasm": "latest",
26
27
  "@tanstack/react-router": "latest",
27
28
  "@tanstack/react-start": "latest",
@@ -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. Components import this and
51
- * call `app.mutate.*` inside event handlers, by which point boot has completed (the provider in
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, <RindleApp> renders the
2
- // merged SSR seed on the server AND through hydration, then boots the in-browser wasm engine
3
- // (client-only) and swaps to the live store — the SSR→SPA handoff. <TopBar> + <Toaster> are the
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 LEAF route owns its own first-paint preload (its loader returns `{ rindle }`); RootDocument
7
- // merges every matched route's slice, so a first visit to any route seeds exactly the queries it renders.
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 { useMemo } from "react";
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
- <RindleApp ssrState={ssrState}>
45
+ <rindle.Provider>
61
46
  <TopBar />
62
47
  <main className="app-main">
63
48
  <Outlet />
64
49
  </main>
65
50
  <Toaster />
66
- </RindleApp>
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 (SSR); after hydration the wasm engine owns
3
- // the live read — post a message in any room and its count here updates with no polling.
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: async (): Promise<{ rindle: DehydratedState }> => {
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
- // single detail query is seeded by the loader for first paint; after hydration the wasm engine owns
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: async ({ params }): Promise<{ rindle: DehydratedState }> => {
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, the browser hydrates it (src/RindleApp.tsx) for an instant correct first paint, then
7
- // the wasm engine boots and the live `subscribe` reconciles.
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 from inside a loader's `import.meta.env.SSR` guard (a dynamic import), keeping it out
11
- // of the client bundle. It never imports the engine.
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. Call from a route loader (server only). `preloadAll` degrades a failed read to no
36
- * seed for that query (the live engine fills it in after hydration) rather than breaking the whole page. */
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
- }