@lunora/cli 1.0.0-alpha.39 → 1.0.0-alpha.390
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/LICENSE.md +33 -0
- package/README.md +1 -1
- package/dist/bin.mjs +2 -10
- package/dist/index.d.mts +1019 -374
- package/dist/index.d.ts +1019 -374
- package/dist/index.mjs +1 -19
- package/dist/packem_chunks/dispatch.mjs +1 -0
- package/dist/packem_chunks/handler.mjs +1 -160
- package/dist/packem_chunks/handler10.mjs +1 -16
- package/dist/packem_chunks/handler11.mjs +1 -22
- package/dist/packem_chunks/handler12.mjs +1 -192
- package/dist/packem_chunks/handler13.mjs +3 -131
- package/dist/packem_chunks/handler14.mjs +1 -65
- package/dist/packem_chunks/handler15.mjs +1 -58
- package/dist/packem_chunks/handler16.mjs +1 -80
- package/dist/packem_chunks/handler17.mjs +7 -43
- package/dist/packem_chunks/handler18.mjs +1 -105
- package/dist/packem_chunks/handler19.mjs +1 -170
- package/dist/packem_chunks/handler2.mjs +1 -114
- package/dist/packem_chunks/handler20.mjs +3 -94
- package/dist/packem_chunks/handler21.mjs +3 -94
- package/dist/packem_chunks/handler22.mjs +1 -0
- package/dist/packem_chunks/handler23.mjs +2 -0
- package/dist/packem_chunks/handler24.mjs +2 -0
- package/dist/packem_chunks/handler25.mjs +99 -0
- package/dist/packem_chunks/handler26.mjs +9 -0
- package/dist/packem_chunks/handler27.mjs +1 -0
- package/dist/packem_chunks/handler3.mjs +1 -204
- package/dist/packem_chunks/handler4.mjs +3 -33
- package/dist/packem_chunks/handler5.mjs +1 -49
- package/dist/packem_chunks/handler6.mjs +1 -91
- package/dist/packem_chunks/handler7.mjs +1 -42
- package/dist/packem_chunks/handler8.mjs +2 -174
- package/dist/packem_chunks/handler9.mjs +1 -315
- package/dist/packem_chunks/runCodegenCommand.mjs +4 -52
- package/dist/packem_chunks/runDeployCommand.mjs +11 -594
- package/dist/packem_chunks/runDevCommand.mjs +2 -0
- package/dist/packem_chunks/runInitCommand.mjs +164 -1432
- package/dist/packem_chunks/runResetCommand.mjs +1 -41
- package/dist/packem_chunks/runRpcCommand.mjs +1 -68
- package/dist/packem_shared/COMMANDS-JClcR9JT.mjs +1 -0
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-CyIsI3S1.mjs +1 -0
- package/dist/packem_shared/EXIT_CODE-D_XCixv8.mjs +1 -0
- package/dist/packem_shared/admin-token-VdUnvnKW.mjs +1 -0
- package/dist/packem_shared/admin-url-DJB8P6Jh.mjs +1 -0
- package/dist/packem_shared/binding-manifest-file-CQ1eQYy1.mjs +2 -0
- package/dist/packem_shared/buildRegistryIndex-Dc8D8AR6.mjs +1 -0
- package/dist/packem_shared/cap-error-body-YBKO32BF.mjs +1 -0
- package/dist/packem_shared/catalog-CahzmDLV.mjs +1 -0
- package/dist/packem_shared/cli-DwEpuZm9.mjs +2 -0
- package/dist/packem_shared/cli-manifest-CRet0egA.mjs +1 -0
- package/dist/packem_shared/cloudflare-credentials-BVmZnDZk.mjs +1 -0
- package/dist/packem_shared/codegen-error-AmH54ofi.mjs +3 -0
- package/dist/packem_shared/command-CAXl2A1S.mjs +1 -0
- package/dist/packem_shared/commands-DgUDxkA8.mjs +13 -0
- package/dist/packem_shared/createLogger-Cl18I8AX.mjs +2 -0
- package/dist/packem_shared/createRecordingSpawner-Ch7zuQQE.mjs +1 -0
- package/dist/packem_shared/deploy-target-DCWwiuAe.mjs +1 -0
- package/dist/packem_shared/diffSnapshots-htqNZT8Q.mjs +7 -0
- package/dist/packem_shared/docker-AqLAkOyY.mjs +1 -0
- package/dist/packem_shared/health-probe-Bw9JFfIy.mjs +1 -0
- package/dist/packem_shared/import-BRM1hs7j.mjs +12 -0
- package/dist/packem_shared/index-oHZBGBwB.mjs +1 -0
- package/dist/packem_shared/insertSchemaExtension-DuiV6cba.mjs +8 -0
- package/dist/packem_shared/lint-ignore-report-DKZagpqk.mjs +2 -0
- package/dist/packem_shared/mcp-config-file-CcDTeXIq.mjs +2 -0
- package/dist/packem_shared/open-url-EnKy--w-.mjs +1 -0
- package/dist/packem_shared/output-format-B4eQHi5y.mjs +2 -0
- package/dist/packem_shared/parseManifest-CwPTKdtS.mjs +1 -0
- package/dist/packem_shared/path-containment-CgxYZggb.mjs +1 -0
- package/dist/packem_shared/plan-Dl57T11P.mjs +5 -0
- package/dist/packem_shared/planDevCommand-D8sm56hO.mjs +1 -0
- package/dist/packem_shared/platform-diagnostics-Cn2g6Jh-.mjs +4 -0
- package/dist/packem_shared/prompt-cancelled-BvsNxg_Q.mjs +1 -0
- package/dist/packem_shared/render-lunora-error-C3OJY5wk.mjs +2 -0
- package/dist/packem_shared/resolve-BrsV4Qyc.mjs +5 -0
- package/dist/packem_shared/resolve-target-C_ZloTvd.mjs +1 -0
- package/dist/packem_shared/runAddCommand-DTvfp4h2.mjs +1 -0
- package/dist/packem_shared/runExportCommand-slAq5BCF.mjs +5 -0
- package/dist/packem_shared/runMigrateGenerateCommand-BwemPIOz.mjs +11 -0
- package/dist/packem_shared/schemaIrToSnapshot-BHuNOfgs.mjs +1 -0
- package/dist/packem_shared/secret-key-6bCftsxj.mjs +1 -0
- package/dist/packem_shared/shared-D-zCOmgY.mjs +1 -0
- package/dist/packem_shared/storage-DWhTX8eS.mjs +1 -0
- package/dist/packem_shared/tui-prompts-B3YwUhGw.mjs +4 -0
- package/dist/packem_shared/variables-CPYyJtsL.mjs +1 -0
- package/dist/packem_shared/wrangler-name-Dsk5K1f-.mjs +1 -0
- package/dist/packem_shared/wrangler-secrets-3chIVv3H.mjs +1 -0
- package/package.json +39 -19
- package/skills/README.md +35 -17
- package/skills/lunora/SKILL.md +154 -64
- package/skills/lunora-create-package/SKILL.md +97 -84
- package/skills/lunora-deploy/SKILL.md +175 -105
- package/skills/lunora-functions/SKILL.md +362 -56
- package/skills/lunora-migration-helper/SKILL.md +162 -137
- package/skills/lunora-performance-audit/SKILL.md +137 -95
- package/skills/lunora-quickstart/SKILL.md +137 -136
- package/skills/lunora-realtime/SKILL.md +140 -99
- package/skills/lunora-setup-auth/SKILL.md +162 -102
- package/skills/lunora-setup-hyperdrive/SKILL.md +114 -97
- package/skills/lunora-setup-hyperdrive-global/SKILL.md +93 -90
- package/skills/lunora-setup-mail/SKILL.md +85 -113
- package/skills/lunora-setup-scheduler/SKILL.md +132 -96
- package/skills/lunora-setup-storage/SKILL.md +189 -95
- package/dist/packem_chunks/planDevCommand.mjs +0 -541
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +0 -397
- package/dist/packem_shared/COMMANDS-B0ftFD_3.mjs +0 -948
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +0 -244
- package/dist/packem_shared/admin-url-4UzT-CI4.mjs +0 -19
- package/dist/packem_shared/api-spec-CtA6ilu4.mjs +0 -13
- package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +0 -38
- package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +0 -31
- package/dist/packem_shared/command-D3lB_4Az.mjs +0 -19
- package/dist/packem_shared/commands-B-gR09Z_.mjs +0 -845
- package/dist/packem_shared/createLogger-B40gPzQo.mjs +0 -78
- package/dist/packem_shared/createRecordingSpawner-DxI3mebw.mjs +0 -43
- package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
- package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +0 -161
- package/dist/packem_shared/docker-hMQ97KSQ.mjs +0 -21
- package/dist/packem_shared/insertSchemaExtension-DAqbfr9Z.mjs +0 -64
- package/dist/packem_shared/open-url-Dfq6fAyT.mjs +0 -41
- package/dist/packem_shared/output-format-wUvAN6AL.mjs +0 -17
- package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
- package/dist/packem_shared/parseManifest--vZf2FY1.mjs +0 -94
- package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +0 -9
- package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +0 -16
- package/dist/packem_shared/runAddCommand-bnY6-HKb.mjs +0 -4
- package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +0 -79
- package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +0 -43
- package/dist/packem_shared/storage-BIsph-Vk.mjs +0 -84
- package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +0 -658
- package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +0 -12
- package/dist/packem_shared/wrangler-secrets-P2_ZUR-k.mjs +0 -47
|
@@ -1,37 +1,30 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lunora-realtime
|
|
3
|
-
description: Wires Lunora's live data into a client.
|
|
4
|
-
reactive `useQuery`/`useSubscription`, `useMutation` with optimistic updates,
|
|
5
|
-
pagination, connection status, the React/Vue/Solid/Svelte adapters, and the
|
|
6
|
-
`@lunora/db` TanStack binding.
|
|
3
|
+
description: Wires Lunora's live data into a client app. Covers `LunoraClient` and the per-framework provider, reactive `useQuery`/`useSubscription`, `useMutation` with optimistic updates, `usePaginatedQuery`, `shardKey` routing, connection status, presence, the React/Vue/Solid/Svelte/Angular/React Native adapters, and the `@lunora/db` TanStack DB binding. Use when building UI that reads or writes Lunora functions, when asked to "show live data", "make this update in real time", "add optimistic updates", "add infinite scroll/pagination", or "show offline/reconnecting state", when editing components that import `@lunora/react`/`vue`/`solid`/`svelte`/`angular`/`react-native` or `lunorash/client`, or when a live query stays `undefined`, freezes, or doesn't update.
|
|
7
4
|
---
|
|
8
5
|
|
|
9
6
|
# Lunora Realtime
|
|
10
7
|
|
|
11
8
|
Consume Lunora functions reactively from the client. Queries are live
|
|
12
|
-
subscriptions over WebSocket — a `useQuery` re-renders
|
|
9
|
+
subscriptions over WebSocket — a `useQuery` re-renders as soon as a mutation
|
|
13
10
|
changes the rows it reads — and mutations can paint optimistically with
|
|
14
11
|
automatic rollback.
|
|
15
12
|
|
|
16
|
-
## When to Use
|
|
17
|
-
|
|
18
|
-
- Wiring a frontend to a Lunora backend (React, Vue, Solid, Svelte).
|
|
19
|
-
- Adding optimistic updates, pagination, or presence to the UI.
|
|
20
|
-
- Choosing between live hooks and the `@lunora/db` collection layer.
|
|
21
|
-
|
|
22
13
|
## When Not to Use
|
|
23
14
|
|
|
24
15
|
- Writing the server functions themselves — that's `lunora-functions`.
|
|
25
16
|
- Initial project/provider scaffolding — that's `lunora-quickstart`.
|
|
17
|
+
- Sign-in UI and the `Authenticated` gates — that's `lunora-setup-auth`.
|
|
26
18
|
|
|
27
19
|
## Provider Setup
|
|
28
20
|
|
|
29
|
-
Create the `LunoraClient` once at module scope
|
|
30
|
-
|
|
21
|
+
Create the `LunoraClient` once at module scope; it owns the WebSocket, the
|
|
22
|
+
optimistic cache, and the offline queue, so creating it inside a component
|
|
23
|
+
reopens the socket on every render.
|
|
31
24
|
|
|
32
25
|
```tsx
|
|
33
|
-
import { LunoraClient } from "@lunora/client";
|
|
34
26
|
import { LunoraProvider } from "@lunora/react";
|
|
27
|
+
import { LunoraClient } from "lunorash/client";
|
|
35
28
|
|
|
36
29
|
const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;
|
|
37
30
|
const client = new LunoraClient({ url });
|
|
@@ -39,80 +32,97 @@ const client = new LunoraClient({ url });
|
|
|
39
32
|
// <LunoraProvider client={client}>…</LunoraProvider>
|
|
40
33
|
```
|
|
41
34
|
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
The adapters differ in how they register the client and name their primitives:
|
|
36
|
+
|
|
37
|
+
| Package | Register the client | Live read / write |
|
|
38
|
+
| ---------------------- | ------------------------------------------------------------ | ---------------------------------------------- |
|
|
39
|
+
| `@lunora/react` | `<LunoraProvider client={client}>` | `useQuery` / `useMutation` |
|
|
40
|
+
| `@lunora/vue` | `app.use(createLunora(client))` (or `provideLunora(client)`) | `useQuery` / `useMutation` |
|
|
41
|
+
| `@lunora/solid` | `<LunoraProvider client={client}>` | `createQuery` / `createMutation` |
|
|
42
|
+
| `@lunora/svelte` | `setLunoraClient(client)` in the root layout | `query` / `mutation` stores |
|
|
43
|
+
| `@lunora/angular` | `provideLunora()` in `ApplicationConfig` | `liveQuery` / `mutate`, `injectLunoraClient()` |
|
|
44
|
+
| `@lunora/react-native` | `createLunoraClient({ url, storage })` + `LunoraProvider` | re-exports `@lunora/react` hooks |
|
|
45
|
+
|
|
46
|
+
The rest of this skill uses the React names; the others mirror the same
|
|
47
|
+
semantics (`usePaginatedQuery` → `createPaginatedQuery` / `paginatedQuery`,
|
|
48
|
+
`useConnectionStatus` → `createConnectionStatus` / `connectionStatus`, …).
|
|
49
|
+
React Native passes `storage: AsyncStorage` so the offline queue survives a
|
|
50
|
+
restart; `@lunora/react-native/auth` bridges a better-auth Expo session.
|
|
51
|
+
`@lunora/astro` and `@lunora/nuxt` (Nitro) integrate Lunora into the SSR server.
|
|
44
52
|
|
|
45
53
|
## Live Queries
|
|
46
54
|
|
|
47
|
-
`useQuery(reference, args)` opens a subscription and returns the
|
|
48
|
-
`undefined` while it loads.
|
|
49
|
-
|
|
55
|
+
`useQuery(reference, args, options?)` opens a subscription and returns the
|
|
56
|
+
value, or `undefined` while it loads. References come from codegen and are
|
|
57
|
+
typed end to end, so no cast is needed:
|
|
50
58
|
|
|
51
59
|
```tsx
|
|
52
60
|
import { useQuery } from "@lunora/react";
|
|
53
61
|
|
|
54
|
-
import { api } from "
|
|
55
|
-
import type { Doc } from "../../lunora/_generated/dataModel";
|
|
62
|
+
import { api } from "../lunora/_generated/api";
|
|
56
63
|
|
|
57
|
-
const todos = useQuery(api.todos.list, {})
|
|
64
|
+
const todos = useQuery(api.todos.list, {});
|
|
65
|
+
const messages = useQuery(api.messages.list, { channelId }, { shardKey: channelId, onError: report });
|
|
58
66
|
```
|
|
59
67
|
|
|
60
|
-
- `undefined` means
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
- `undefined` means loading and `[]` means loaded-and-empty; branch on both.
|
|
69
|
+
- Pass `"skip"` as `args` to disable the query (no network call, no
|
|
70
|
+
subscription) — e.g. until a required id is known.
|
|
71
|
+
- Pass `onError` to see a server-pushed error (an RLS denial, a query that starts
|
|
72
|
+
throwing). Without it the error is dropped and the value silently freezes at
|
|
73
|
+
its last good result.
|
|
74
|
+
- `shardKey`: for a `.shardBy(...)` table, pin every read and write to the
|
|
75
|
+
shard's key (`{ shardKey: channelId }` on both `useQuery` and the mutation
|
|
76
|
+
call). Without it the runtime has to fan out across every shard.
|
|
77
|
+
- The subscription re-runs only when `args` or the rows it read change. Keep
|
|
78
|
+
`args` narrow so unrelated writes don't re-push data (see
|
|
79
|
+
`lunora-performance-audit`).
|
|
80
|
+
- `useSubscription` is the push-only variant (no initial HTTP fetch) returning
|
|
81
|
+
`{ data, error }`. For SSR, `@lunora/react/server` exports `preloadQuery`,
|
|
82
|
+
`fetchQuery`, and `lunoraQueryOptions`; hand the result to
|
|
83
|
+
`usePreloadedQuery` on the client.
|
|
84
|
+
- References in folders are nested: `lunora/billing/invoices.ts` →
|
|
85
|
+
`api.billing.invoices.<fn>`. Client code only ever imports `api`;
|
|
86
|
+
`internal` functions are not callable from the client.
|
|
67
87
|
|
|
68
88
|
## Authorization & Live Queries
|
|
69
89
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
every write-driven refresh) does not — it evaluates as anonymous.
|
|
90
|
+
Live queries are identity-aware. At the WebSocket upgrade the runtime stamps
|
|
91
|
+
the caller's verified identity onto the socket, and every subscription re-run
|
|
92
|
+
executes under that identity, so:
|
|
74
93
|
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
- `.use(rls(...))` and `ctx.auth.userId` behave the same in a subscription as in
|
|
95
|
+
a one-shot read. Shape subscriptions AND-merge their predicate with the
|
|
96
|
+
table's RLS read policy.
|
|
97
|
+
- An anonymous socket carries no identity, so an RLS/`ctx.auth` query fails
|
|
98
|
+
closed (empty or denied) rather than leaking rows.
|
|
99
|
+
- When the credential carries an expiry, the server sends `TOKEN_EXPIRED` and
|
|
100
|
+
closes with code `4001`. `LunoraClient` reconnects and re-resolves identity
|
|
101
|
+
automatically; surface it in the UI only if a re-login is required.
|
|
77
102
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
**live** updates evaluate anonymously and may resolve to an empty/denied
|
|
81
|
-
set.
|
|
82
|
-
- This **fails closed** — the live channel shows _less_ data, never another
|
|
83
|
-
user's data, so there is no leak. But it is a correctness caveat: the
|
|
84
|
-
initial render and the live updates can disagree.
|
|
85
|
-
|
|
86
|
-
The supported pattern today is to scope per-user data **outside** of
|
|
87
|
-
`ctx.auth` inside a subscribed query:
|
|
88
|
-
|
|
89
|
-
- Partition the data by shard with `.shardBy(userId)` (or tenant/room), so the
|
|
90
|
-
subscription is already scoped to the right state, or
|
|
91
|
-
- Pass the identifier as an **explicit query arg**
|
|
92
|
-
(`useQuery(api.todos.list, { userId })`) and filter on the arg rather than on
|
|
93
|
-
`ctx.auth`.
|
|
94
|
-
|
|
95
|
-
Reserve `rls()` / `ctx.auth`-based filtering for non-subscribed reads (one-shot
|
|
96
|
-
actions/queries) where identity is always present.
|
|
103
|
+
Use `.shardBy(...)` and narrow args for performance, `rls()` / `ctx.auth` for
|
|
104
|
+
authorization. They compose.
|
|
97
105
|
|
|
98
106
|
## Mutations + Optimistic Updates
|
|
99
107
|
|
|
100
|
-
`useMutation(reference)` returns `{ mutate, pending
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
`useMutation(reference)` returns `{ mutate, pending, data, error, reset }`.
|
|
109
|
+
Destructure it — the hook result itself isn't callable. `mutate` returns a
|
|
110
|
+
promise that rejects on failure. Pass `optimisticUpdate` to paint the next state
|
|
111
|
+
immediately; if the server rejects the call the cache rolls back.
|
|
103
112
|
|
|
104
113
|
```tsx
|
|
105
114
|
import { useMutation } from "@lunora/react";
|
|
106
115
|
|
|
107
|
-
import
|
|
116
|
+
import { api } from "../lunora/_generated/api";
|
|
117
|
+
import type { Doc, Id } from "../lunora/_generated/dataModel";
|
|
108
118
|
|
|
109
119
|
const { mutate: add, pending } = useMutation(api.todos.add);
|
|
110
120
|
|
|
111
121
|
await add(
|
|
112
122
|
{ text },
|
|
113
123
|
{
|
|
114
|
-
|
|
115
|
-
const list = (
|
|
124
|
+
optimisticUpdate: (store) => {
|
|
125
|
+
const list = store.getQuery(api.todos.list, {}) ?? [];
|
|
116
126
|
const provisional: Doc<"todos"> = {
|
|
117
127
|
_id: `optimistic_${Date.now()}` as Id<"todos">,
|
|
118
128
|
_creationTime: Date.now(),
|
|
@@ -120,58 +130,89 @@ await add(
|
|
|
120
130
|
done: false,
|
|
121
131
|
createdAt: Date.now(),
|
|
122
132
|
};
|
|
123
|
-
|
|
133
|
+
store.setQuery(api.todos.list, {}, [provisional, ...list]);
|
|
124
134
|
},
|
|
125
135
|
},
|
|
126
136
|
);
|
|
127
137
|
```
|
|
128
138
|
|
|
129
|
-
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
|
|
139
|
+
- `optimisticUpdate` names the query it patches because nothing can infer that
|
|
140
|
+
`todos.add` affects `todos.list`. `store.getAllQueries(fn)` lists every
|
|
141
|
+
subscribed args variant of a query so you can patch each one.
|
|
142
|
+
- The provisional row must match the query's element shape, including
|
|
143
|
+
`_id`/`_creationTime`, or the UI flickers when the real row lands.
|
|
144
|
+
- The per-call `optimistic: (current) => next` shortcut only patches a
|
|
145
|
+
subscription under the mutation's own reference and args (a counter, a doc by
|
|
146
|
+
id). For the add-updates-list shape above it is a silent no-op.
|
|
147
|
+
- `pending` stays `true` until every overlapping call from this hook settles;
|
|
148
|
+
use it to disable the submit button.
|
|
149
|
+
- Offline, mutations are queued by `LunoraClient` and replayed on reconnect,
|
|
150
|
+
keyed by client id so they aren't applied twice.
|
|
137
151
|
|
|
138
152
|
## Pagination, Connection, Presence
|
|
139
153
|
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
The server query takes `paginationOpts` and returns `.paginate(...)` (see
|
|
155
|
+
`lunora-functions`). The hook supplies the cursor:
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
const { results, status, loadMore, error } = usePaginatedQuery(api.messages.page, { channelId }, { initialNumItems: 20 });
|
|
159
|
+
// status: "LoadingFirstPage" | "CanLoadMore" | "LoadingMore" | "Exhausted"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Pages are live ranges: inserts and deletes inside a loaded page grow or shrink
|
|
163
|
+
it in place without duplicating rows. There is no total count; subscribe to a
|
|
164
|
+
separate count query for "42 of 1,203". `useInfiniteQuery` is the
|
|
165
|
+
TanStack-style alternative.
|
|
166
|
+
|
|
142
167
|
- `useConnectionStatus` — live socket state for an offline/reconnecting banner.
|
|
143
|
-
- `usePresence` — who's-here
|
|
168
|
+
- `usePresence` — who's-here with heartbeat (pairs with the `presence` registry
|
|
144
169
|
item).
|
|
145
|
-
- `useAuth` +
|
|
146
|
-
|
|
170
|
+
- `useAuth` + `Authenticated` / `Unauthenticated` / `AuthLoading` — see
|
|
171
|
+
`lunora-setup-auth`.
|
|
147
172
|
|
|
148
173
|
## `@lunora/db` — TanStack DB Collections
|
|
149
174
|
|
|
150
|
-
For
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
175
|
+
For indexed local collections, cross-query joins, or a durable offline outbox,
|
|
176
|
+
use `@lunora/db` (peer deps `@tanstack/db`, `@tanstack/offline-transactions`).
|
|
177
|
+
Plain `useQuery` / `useMutation` are enough for straightforward live lists.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { defineCollections } from "@lunora/db";
|
|
181
|
+
import type { LunoraClient } from "lunorash/client";
|
|
182
|
+
|
|
183
|
+
import { api } from "../lunora/_generated/api";
|
|
184
|
+
import type { Doc, Id } from "../lunora/_generated/dataModel";
|
|
185
|
+
|
|
186
|
+
export const createCollections = (client: LunoraClient) =>
|
|
187
|
+
defineCollections(client, {
|
|
188
|
+
messages: {
|
|
189
|
+
list: api.messages.list,
|
|
190
|
+
scopeBy: "channelId", // the .shardBy column
|
|
191
|
+
insert: {
|
|
192
|
+
mutation: api.messages.send,
|
|
193
|
+
optimistic: (input: Omit<Doc<"messages">, "_id" | "_creationTime">, id) => ({ _id: id as Id<"messages">, _creationTime: Date.now(), ...input }),
|
|
194
|
+
toArgs: (row) => ({ channelId: row.channelId, id: row._id, text: row.text }),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
// → db.collections.* (feed useLiveQuery), db.actions.* (optimistic writes),
|
|
199
|
+
// db.scope.* (re-point sharded collections), db.executor (the outbox)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Call `defineCollections` once and treat its collections as the single source of
|
|
203
|
+
truth per table. Optimistic writes and sync deltas land only on that instance,
|
|
204
|
+
so a second instance or a copy of the rows in your own store drifts: derived
|
|
205
|
+
indexes (trees, search, undo captures) built from the copy miss new rows and
|
|
206
|
+
hold stale ones, while `useLiveQuery` still looks correct.
|
|
168
207
|
|
|
169
208
|
## Checklist
|
|
170
209
|
|
|
171
|
-
- [ ] `LunoraClient` created once at module scope
|
|
172
|
-
|
|
173
|
-
- [ ]
|
|
174
|
-
|
|
175
|
-
- [ ]
|
|
176
|
-
- [ ]
|
|
177
|
-
|
|
210
|
+
- [ ] `LunoraClient` created once at module scope and registered the way the
|
|
211
|
+
adapter expects.
|
|
212
|
+
- [ ] `undefined` handled as loading; `"skip"` used while args are incomplete;
|
|
213
|
+
`onError` wired where a failure must be visible.
|
|
214
|
+
- [ ] Reads and writes on `.shardBy` tables pass `shardKey`.
|
|
215
|
+
- [ ] Mutations destructure `mutate`; `pending` disables submit;
|
|
216
|
+
`optimisticUpdate` rows match the query's element shape.
|
|
217
|
+
- [ ] Lists that grow use `usePaginatedQuery` over a `.paginate` query.
|
|
218
|
+
- [ ] `@lunora/db`: one `defineCollections` instance; no parallel copy of rows.
|