@rindle/react 0.1.6 → 0.2.0

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
@@ -3,7 +3,8 @@
3
3
  React bindings for Rindle stores.
4
4
 
5
5
  ```tsx
6
- import { Rindle, useQuery } from "@rindle/react";
6
+ import { Rindle, fragmentKey, useFragment, useRoot } from "@rindle/react";
7
+ import { IssueCardFragment, issuesPageQuery, type IssueCardRef } from "./IssueListItem.queries.ts";
7
8
 
8
9
  createRoot(root).render(
9
10
  <Rindle store={app.store}>
@@ -12,12 +13,22 @@ createRoot(root).render(
12
13
  );
13
14
 
14
15
  function Issues() {
15
- const issues = useQuery(queries.issuesPage(null));
16
- return issues.map((issue) => <div key={issue.id}>{issue.title}</div>);
16
+ const [issues, { status }] = useRoot(issuesPageQuery, { limit: 50 }, IssueCardFragment);
17
+ if (status !== "complete" && issues.length === 0) return <p>Loading...</p>;
18
+ return issues.map((issue) => <IssueRow key={fragmentKey(issue)} issue={issue} />);
19
+ }
20
+
21
+ function IssueRow({ issue }: { issue: IssueCardRef }) {
22
+ const data = useFragment(IssueCardFragment, issue);
23
+ if (!data) return null;
24
+ return <div>{data.title}</div>;
17
25
  }
18
26
  ```
19
27
 
20
- `useQuery(query)` returns live `data`, subscribes on mount, and releases on unmount.
28
+ `useRoot(namedQuery, args?, ...ctx?, RootFragment?)` retains the full named root query for sync
29
+ coverage and returns local React-facing data. Passing a root fragment returns opaque refs for the
30
+ root rows; child components read those refs with `useFragment(fragment, ref)`. `useQuery(query)` is
31
+ still available for direct live query reads that do not need fragment-local boundaries.
21
32
 
22
33
  For local-first backends, the React-facing data view is shared by compiled local AST. Each
23
34
  mounted hook still retains its own `name`/`args` lease through the Store, so backend-level
@@ -33,10 +44,20 @@ cannot split local views from remote subscriptions keep the older materialized-v
33
44
  answered. A pending optimistic mutation no longer moves it — "is a write pending here?" is a
34
45
  separate axis on the backend (`backend.pending(qid)` / `onPending`), and `error` is reserved
35
46
  and currently unproduced. Shares `useQuery`'s leased view, so reading both is one subscription.
36
- - `useFragment(fragment, ref)` — a pure read of an already-projected fragment slice off the root
37
- query's view (FRAGMENT-COMPOSITION-DESIGN.md §7). Adds no lease and never re-renders on its own;
38
- it keeps a component tree waterfall-free by letting descendants read the one query the root opened.
47
+ - `useSyncQuery(query)` — retains a named remote query for normalized/local-first sync coverage
48
+ without subscribing React to the broad result tree; returns only `ResultType`.
49
+ - `useRoot(namedQuery, args?, ...ctx?)` retains the full root coverage query and returns
50
+ `[data, details]`, where `details.status` distinguishes loading from completed-not-found.
51
+ Without a fragment argument, `data` is the root query's local React-facing data. Fragment child
52
+ relationships are surfaced as `FragmentRef`s, so children can keep owning their own reads.
53
+ Passing a fragment as the final argument switches `data` to root-ref mode:
54
+ `useRoot(namedQuery, args, RootFragment)`.
55
+ - `useFragment(fragment, ref)` — reads an opaque local fragment ref. It opens a narrow local-only
56
+ query for that fragment and returns `null` until the local row is available. Child relationships
57
+ composed as `sub(alias, rel, ChildFragment)` are refs; inline child builders remain inline data.
58
+ - `fragmentKey(ref)` — stable key for opaque fragment refs, useful for React list keys.
39
59
  - `useRindleStore()` — the `Store` from context, for imperative writes/reads inside components.
60
+ - `SyncQueryCache` — the per-provider cache that dedupes sync-only coverage retains.
40
61
  - `QueryCache` — the per-provider cache that dedupes a query's materialized view across hooks.
41
62
  - `queryCacheKey(query)` — the stable string key (`@rindle/client`'s `stableKey(ast)`) under which
42
63
  the cache stores that view. Same AST → same key → one shared entry, and it matches the SSR seed
package/dist/index.d.ts CHANGED
@@ -1,9 +1,17 @@
1
1
  import type { ReactNode } from "react";
2
- import type { ColsMap, Fragment, FragmentRef, Query, ResultType, Store } from "@rindle/client";
3
- type AnyQuery = Query<any, any, any>;
2
+ import type { AnyQuery, ColsMap, Fragment, FragmentData, FragmentRef, NamedQuery, QueryLocalData, ResultType, Store } from "@rindle/client";
3
+ type AnyFragment = Fragment<any, any, any, any>;
4
4
  export type QueryData<Q extends AnyQuery> = ReturnType<Q["materialize"]>["data"];
5
+ export type RootData<Q extends AnyQuery> = QueryLocalData<Q>;
6
+ export type RootRefData<Q extends AnyQuery, F extends Fragment<any, any, any, any>> = QueryData<Q> extends readonly unknown[] ? readonly FragmentRef<F>[] : FragmentRef<F> | null;
7
+ export interface RootDetails {
8
+ readonly status: ResultType;
9
+ }
10
+ export type RootResult<Q extends AnyQuery> = readonly [data: RootData<Q>, details: RootDetails];
11
+ export type RootRefResult<Q extends AnyQuery, F extends Fragment<any, any, any, any>> = readonly [data: RootRefData<Q, F>, details: RootDetails];
5
12
  export type { ResultType } from "@rindle/client";
6
- export type { Fragment, FragmentRef } from "@rindle/client";
13
+ export type { Fragment, FragmentData, FragmentRef } from "@rindle/client";
14
+ export { fragmentKey } from "@rindle/client";
7
15
  export interface RindleProps<S extends ColsMap = ColsMap> {
8
16
  store: Store<S>;
9
17
  children?: ReactNode;
@@ -12,9 +20,14 @@ interface QueryLease {
12
20
  id: number;
13
21
  viewKey: string;
14
22
  }
23
+ interface SyncLease {
24
+ id: number;
25
+ coverageKey: string;
26
+ }
15
27
  declare class RindleContextValue {
16
28
  readonly store: Store<ColsMap>;
17
29
  readonly cache: QueryCache;
30
+ readonly syncCache: SyncQueryCache;
18
31
  constructor(store: Store<ColsMap>);
19
32
  }
20
33
  export declare function Rindle<S extends ColsMap>({ store, children }: RindleProps<S>): import("react").FunctionComponentElement<import("react").ProviderProps<RindleContextValue | null>>;
@@ -28,22 +41,44 @@ export declare function useQuery<Q extends AnyQuery>(query: Q): QueryData<Q>;
28
41
  * as {@link useQuery} (so reading both for one query is one subscription), and re-renders only when
29
42
  * the status changes. */
30
43
  export declare function useQueryStatus(query: AnyQuery): ResultType;
44
+ /** Retain a named server query for normalized/local-first sync coverage without subscribing React
45
+ * to that query's broad result tree. The returned value is lifecycle state only; it is `unknown`
46
+ * until the backend reports that the retained coverage has hydrated. */
47
+ export declare function useSyncQuery(query: AnyQuery): ResultType;
48
+ /** Run a named root query and expose its local React-facing data. Fragment child relationships are
49
+ * refs, so child components can keep owning their own local reads. Passing a root fragment as the
50
+ * final argument switches the result to opaque root refs for that fragment. */
51
+ export declare function useRoot<Q extends AnyQuery>(query: Q): RootResult<Q>;
52
+ export declare function useRoot<Q extends AnyQuery, F extends AnyFragment>(query: Q, fragment: F): RootRefResult<Q, F>;
53
+ export declare function useRoot<Q extends AnyQuery>(query: NamedQuery<void, [], Q>): RootResult<Q>;
54
+ export declare function useRoot<Q extends AnyQuery, F extends AnyFragment>(query: NamedQuery<void, [], Q>, fragment: F): RootRefResult<Q, F>;
55
+ export declare function useRoot<Args, Ctx extends readonly unknown[], Q extends AnyQuery>(query: NamedQuery<Args, Ctx, Q>, args: Args, ...ctx: Ctx): RootResult<Q>;
56
+ export declare function useRoot<Args, Ctx extends readonly unknown[], Q extends AnyQuery, F extends AnyFragment>(query: NamedQuery<Args, Ctx, Q>, args: Args, ...ctxAndFragment: [...ctx: Ctx, fragment: F]): RootRefResult<Q, F>;
31
57
  /**
32
- * Read a {@link Fragment}'s slice off the shared root view (FRAGMENT-COMPOSITION-DESIGN.md §7).
58
+ * Read a {@link Fragment}'s local data from an opaque ref.
33
59
  *
34
- * The waterfall-free contract: the ROOT component `useQuery`s the composed query (every child
35
- * fragment spread into it), and every descendant calls `useFragment` a PURE READ of the
36
- * already-projected node at this fragment's alias instead of opening its own `useQuery`. So a
37
- * whole component tree resolves through the ONE query the root opened, with no request waterfall;
38
- * a descendant that opens its own `useQuery` reintroduces the waterfall at that boundary.
60
+ * The query boundary calls {@link useRoot} with a fragment argument to retain the full named
61
+ * coverage query and receive root refs. Descendants call `useFragment` with those refs (or child
62
+ * refs returned by a parent fragment read) to open narrow local-only reads for the fields their
63
+ * fragment owns.
39
64
  *
40
- * Phase 0 is a typed pass-through: `ref` already IS the projected node (the parent got it from
41
- * `useQuery` and handed the slice down), so this returns it typed as the fragment's
42
- * {@link FragmentRef}. `fragment` declares *which* selection this component reads (co-location +
43
- * the seam masking plugs into); masking (Phase 2) narrows the returned shape with no call-site
44
- * change. Not a subscription — it adds no lease and never re-renders on its own.
65
+ * `ref` is an opaque token created by {@link useRoot} or returned from another local fragment read.
66
+ * The hook opens a narrow local-only query for this exact fragment and keeps the root coverage
67
+ * lease retained while mounted. Passing a legacy projected data object is unsupported.
45
68
  */
46
- export declare function useFragment<F extends Fragment<any, any, any>>(fragment: F, ref: FragmentRef<F>): FragmentRef<F>;
69
+ export declare function useFragment<F extends Fragment<any, any, any, any>>(fragment: F, ref: FragmentRef<F> | null | undefined): FragmentData<F> | null;
70
+ export declare class SyncQueryCache {
71
+ private readonly entries;
72
+ private nextLeaseId;
73
+ private readonly store;
74
+ constructor(store: Store<ColsMap>);
75
+ retain(coverageKey: string, query: AnyQuery): SyncLease;
76
+ release(lease: SyncLease): void;
77
+ subscribe(coverageKey: string, listener: () => void): () => void;
78
+ resultType(coverageKey: string): ResultType;
79
+ size(): number;
80
+ private createHandle;
81
+ }
47
82
  export declare class QueryCache {
48
83
  private readonly entries;
49
84
  private nextLeaseId;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  import { createContext, createElement, useCallback, useContext, useMemo, useRef, useSyncExternalStore, } from "react";
2
- import { stableKey } from "@rindle/client";
2
+ import { createLocalFragmentRefForTable, fragmentAst, isFragment, isFragmentRelationship, localQueryReadAst, localFragmentReadAst, localRootFragmentRefsAst, queryFromAst, stableKey, tableMeta, } from "@rindle/client";
3
+ export { fragmentKey } from "@rindle/client";
3
4
  const EMPTY_ARRAY = Object.freeze([]);
4
5
  class RindleContextValue {
5
6
  store;
6
7
  cache;
8
+ syncCache;
7
9
  constructor(store) {
8
10
  this.store = store;
9
11
  this.cache = new QueryCache(store);
12
+ this.syncCache = new SyncQueryCache(store);
10
13
  }
11
14
  }
12
15
  const RindleContext = createContext(null);
@@ -62,24 +65,450 @@ export function useQueryStatus(query) {
62
65
  const getServerSnapshot = useCallback(() => ctx.cache.serverResultType(descriptor.viewKey), [ctx.cache, descriptor.viewKey]);
63
66
  return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
64
67
  }
68
+ /** Retain a named server query for normalized/local-first sync coverage without subscribing React
69
+ * to that query's broad result tree. The returned value is lifecycle state only; it is `unknown`
70
+ * until the backend reports that the retained coverage has hydrated. */
71
+ export function useSyncQuery(query) {
72
+ const ctx = useRindleContext();
73
+ const descriptor = useMemo(() => describeQuery(query), [query]);
74
+ const queryRef = useRef(query);
75
+ queryRef.current = query;
76
+ const subscribe = useCallback((onStoreChange) => {
77
+ const lease = ctx.syncCache.retain(descriptor.leaseKey, queryRef.current);
78
+ const unsubscribe = ctx.syncCache.subscribe(descriptor.leaseKey, onStoreChange);
79
+ return () => {
80
+ unsubscribe();
81
+ ctx.syncCache.release(lease);
82
+ };
83
+ }, [ctx.syncCache, descriptor.leaseKey]);
84
+ const getSnapshot = useCallback(() => {
85
+ const live = ctx.syncCache.resultType(descriptor.leaseKey);
86
+ return live === "unknown" && ctx.cache.serverResultType(descriptor.viewKey) === "complete" ? "complete" : live;
87
+ }, [ctx.cache, ctx.syncCache, descriptor.leaseKey, descriptor.viewKey]);
88
+ const getServerSnapshot = useCallback(() => ctx.cache.serverResultType(descriptor.viewKey), [ctx.cache, descriptor.viewKey]);
89
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
90
+ }
91
+ export function useRoot(queryOrNamed, ...args) {
92
+ const { query, fragment } = resolveRootQueryInput(queryOrNamed, args, "useRoot");
93
+ const stableQuery = useStableQuery(query);
94
+ const status = useSyncQuery(stableQuery);
95
+ const data = fragment === undefined
96
+ ? useLocalRootQueryData(stableQuery)
97
+ : useRootRefData(stableQuery, fragment);
98
+ const details = useMemo(() => ({ status }), [status]);
99
+ return useMemo(() => [data, details], [data, details]);
100
+ }
101
+ function resolveRootQueryInput(queryOrNamed, args, hookName) {
102
+ const last = args[args.length - 1];
103
+ const fragment = isFragment(last) ? last : undefined;
104
+ const queryArgs = fragment === undefined ? args : args.slice(0, -1);
105
+ if (isNamedQuery(queryOrNamed)) {
106
+ const query = queryArgs.length === 0
107
+ ? queryOrNamed()
108
+ : queryOrNamed(queryArgs[0], ...queryArgs.slice(1));
109
+ return { query, fragment };
110
+ }
111
+ if (queryArgs.length !== 0)
112
+ throw new Error(`${hookName}(): expected (query) or (query, fragment).`);
113
+ return { query: queryOrNamed, fragment };
114
+ }
115
+ function isNamedQuery(v) {
116
+ return typeof v === "function"
117
+ && typeof v.queryName === "string"
118
+ && typeof v.resolve === "function";
119
+ }
120
+ function useStableQuery(query) {
121
+ const descriptor = describeQuery(query);
122
+ const ref = useRef(undefined);
123
+ if (ref.current === undefined || ref.current.leaseKey !== descriptor.leaseKey) {
124
+ ref.current = { leaseKey: descriptor.leaseKey, query };
125
+ }
126
+ return ref.current.query;
127
+ }
128
+ function useLocalRootQueryData(query) {
129
+ const ctx = useRindleContext();
130
+ const descriptor = useMemo(() => describeQuery(query), [query]);
131
+ const ast = useMemo(() => localQueryReadAst(query, (table) => ctx.store.primaryKeyFor(table)), [ctx.store, query]);
132
+ const localQuery = useMemo(() => queryFromAst(ast), [ast]);
133
+ const localDescriptor = useMemo(() => describeQuery(localQuery), [localQuery]);
134
+ const projection = useMemo(() => new LocalFragmentProjection(ast, { key: descriptor.leaseKey, query }, (table) => ctx.store.primaryKeyFor(table)), [ctx.store, ast, descriptor.leaseKey, query]);
135
+ const subscribe = useCallback((onStoreChange) => {
136
+ const localLease = ctx.cache.retain(localDescriptor.viewKey, localQuery);
137
+ const unsubscribeLocal = ctx.cache.subscribe(localDescriptor.viewKey, onStoreChange);
138
+ return () => {
139
+ unsubscribeLocal();
140
+ ctx.cache.release(localLease);
141
+ };
142
+ }, [ctx.cache, localDescriptor.viewKey, localQuery]);
143
+ // Stale-while-revalidate: render whatever the local IVM view already holds (synced rows,
144
+ // optimistic writes, partially-covered rows) rather than blanking to empty until the server
145
+ // confirms coverage. `status` (from useSyncQuery) stays a SEPARATE signal callers can gate on;
146
+ // the data itself never waits on the round-trip, so navigating to a view that's locally warm
147
+ // shows rows immediately instead of flashing a loading state. (Eviction-on-release still drops
148
+ // a cold view; a release TTL to keep views warm across nav is future work.)
149
+ const getSnapshot = useCallback(() => {
150
+ if (ctx.cache.serverResultType(descriptor.viewKey) === "complete") {
151
+ return projectLocalRootSnapshot(ctx.cache.serverSnapshot(descriptor.viewKey, descriptor.one), descriptor.one, projection);
152
+ }
153
+ return projectLocalRootSnapshot(ctx.cache.snapshot(localDescriptor.viewKey, descriptor.one), descriptor.one, projection);
154
+ }, [ctx.cache, descriptor.one, descriptor.viewKey, localDescriptor.viewKey, projection]);
155
+ const getServerSnapshot = useCallback(() => projectLocalRootSnapshot(ctx.cache.serverSnapshot(descriptor.viewKey, descriptor.one), descriptor.one, projection), [ctx.cache, descriptor.one, descriptor.viewKey, projection]);
156
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
157
+ }
158
+ function useRootRefData(query, fragment) {
159
+ const ctx = useRindleContext();
160
+ const descriptor = useMemo(() => describeQuery(query), [query]);
161
+ const ast = useMemo(() => localRootFragmentRefsAst(fragment, query, (table) => ctx.store.primaryKeyFor(table)), [ctx.store, fragment, query]);
162
+ const localQuery = useMemo(() => queryFromAst(ast), [ast]);
163
+ const localDescriptor = useMemo(() => describeQuery(localQuery), [localQuery]);
164
+ const projection = useMemo(() => new RootRefProjection(fragment, { key: descriptor.leaseKey, query }, (table) => ctx.store.primaryKeyFor(table)), [ctx.store, descriptor.leaseKey, fragment, query]);
165
+ const subscribe = useCallback((onStoreChange) => {
166
+ const localLease = ctx.cache.retain(localDescriptor.viewKey, localQuery);
167
+ const unsubscribeLocal = ctx.cache.subscribe(localDescriptor.viewKey, onStoreChange);
168
+ return () => {
169
+ unsubscribeLocal();
170
+ ctx.cache.release(localLease);
171
+ };
172
+ }, [ctx.cache, localDescriptor.viewKey, localQuery]);
173
+ // Stale-while-revalidate (see useLocalRootQueryData): the root rows render from the live local
174
+ // view as soon as it holds anything; `status` is the separate axis for "server-authoritative yet".
175
+ const getSnapshot = useCallback(() => {
176
+ if (ctx.cache.serverResultType(descriptor.viewKey) === "complete") {
177
+ return projectRootRefSnapshot(ctx.cache.serverSnapshot(descriptor.viewKey, descriptor.one), descriptor.one, projection);
178
+ }
179
+ const raw = ctx.cache.snapshot(localDescriptor.viewKey, descriptor.one);
180
+ return descriptor.one ? projection.projectOne(raw) : projection.projectMany(raw);
181
+ }, [ctx.cache, descriptor.one, descriptor.viewKey, localDescriptor.viewKey, projection]);
182
+ const getServerSnapshot = useCallback(() => projectRootRefSnapshot(ctx.cache.serverSnapshot(descriptor.viewKey, descriptor.one), descriptor.one, projection), [ctx.cache, descriptor.one, descriptor.viewKey, projection]);
183
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
184
+ }
65
185
  /**
66
- * Read a {@link Fragment}'s slice off the shared root view (FRAGMENT-COMPOSITION-DESIGN.md §7).
186
+ * Read a {@link Fragment}'s local data from an opaque ref.
67
187
  *
68
- * The waterfall-free contract: the ROOT component `useQuery`s the composed query (every child
69
- * fragment spread into it), and every descendant calls `useFragment` a PURE READ of the
70
- * already-projected node at this fragment's alias instead of opening its own `useQuery`. So a
71
- * whole component tree resolves through the ONE query the root opened, with no request waterfall;
72
- * a descendant that opens its own `useQuery` reintroduces the waterfall at that boundary.
188
+ * The query boundary calls {@link useRoot} with a fragment argument to retain the full named
189
+ * coverage query and receive root refs. Descendants call `useFragment` with those refs (or child
190
+ * refs returned by a parent fragment read) to open narrow local-only reads for the fields their
191
+ * fragment owns.
73
192
  *
74
- * Phase 0 is a typed pass-through: `ref` already IS the projected node (the parent got it from
75
- * `useQuery` and handed the slice down), so this returns it typed as the fragment's
76
- * {@link FragmentRef}. `fragment` declares *which* selection this component reads (co-location +
77
- * the seam masking plugs into); masking (Phase 2) narrows the returned shape with no call-site
78
- * change. Not a subscription — it adds no lease and never re-renders on its own.
193
+ * `ref` is an opaque token created by {@link useRoot} or returned from another local fragment read.
194
+ * The hook opens a narrow local-only query for this exact fragment and keeps the root coverage
195
+ * lease retained while mounted. Passing a legacy projected data object is unsupported.
79
196
  */
80
197
  export function useFragment(fragment, ref) {
81
- void fragment;
82
- return ref;
198
+ return useLocalFragment(fragment, ref);
199
+ }
200
+ function useLocalFragment(fragment, ref) {
201
+ const ctx = useRindleContext();
202
+ const coverage = ref?.coverage;
203
+ const coverageDescriptor = useMemo(() => (coverage ? describeQuery(coverage.query) : undefined), [coverage]);
204
+ const ast = useMemo(() => (ref ? localFragmentReadAst(fragment, ref, (table) => ctx.store.primaryKeyFor(table)) : undefined), [ctx.store, fragment, ref]);
205
+ const query = useMemo(() => (ast ? queryFromAst(ast) : undefined), [ast]);
206
+ const descriptor = useMemo(() => (query ? describeQuery(query) : undefined), [query]);
207
+ const projection = useMemo(() => (coverage ? new LocalFragmentProjection(fragmentAst(fragment), coverage, (table) => ctx.store.primaryKeyFor(table)) : undefined), [ctx.store, coverage, fragment]);
208
+ const subscribe = useCallback((onStoreChange) => {
209
+ if (!coverage || !descriptor || !query)
210
+ return () => { };
211
+ const syncLease = ctx.syncCache.retain(coverage.key, coverage.query);
212
+ const localLease = ctx.cache.retain(descriptor.viewKey, query);
213
+ const unsubscribeSync = ctx.syncCache.subscribe(coverage.key, onStoreChange);
214
+ const unsubscribeLocal = ctx.cache.subscribe(descriptor.viewKey, onStoreChange);
215
+ return () => {
216
+ unsubscribeLocal();
217
+ unsubscribeSync();
218
+ ctx.cache.release(localLease);
219
+ ctx.syncCache.release(syncLease);
220
+ };
221
+ }, [ctx.cache, ctx.syncCache, coverage, descriptor, query]);
222
+ // Stale-while-revalidate (see useLocalRootQueryData): project the fragment's local view as soon
223
+ // as the row exists locally instead of returning null until its coverage is server-complete —
224
+ // otherwise every nested fragment (UserBadge, TagChip, CommentCard, …) flashes empty for a
225
+ // round-trip on each navigation. A field not yet synced simply reads absent, not "loading".
226
+ const getSnapshot = useCallback(() => {
227
+ if (!coverage || !descriptor || !projection)
228
+ return null;
229
+ if (coverageDescriptor && ctx.cache.serverResultType(coverageDescriptor.viewKey) === "complete") {
230
+ return projectFragmentSeed(ctx, coverage.query.ast(), coverageDescriptor, ref, projection);
231
+ }
232
+ const data = projection.project(ctx.cache.snapshot(descriptor.viewKey, true));
233
+ return data;
234
+ }, [ctx, coverage, coverageDescriptor, descriptor, projection, ref]);
235
+ const getServerSnapshot = useCallback(() => projectFragmentSeed(ctx, coverage?.query.ast(), coverageDescriptor, ref, projection), [ctx, coverage, coverageDescriptor, projection, ref]);
236
+ return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
237
+ }
238
+ function projectRootRefSnapshot(raw, one, projection) {
239
+ return one ? projection.projectOne(raw) : projection.projectMany(raw);
240
+ }
241
+ function projectLocalRootSnapshot(raw, one, projection) {
242
+ return one ? projection.projectOne(raw) : projection.projectMany(raw);
243
+ }
244
+ function projectFragmentSeed(ctx, coverageAst, coverageDescriptor, ref, projection) {
245
+ if (!coverageAst || !coverageDescriptor || !ref || !projection)
246
+ return null;
247
+ const raw = ctx.cache.serverSnapshot(coverageDescriptor.viewKey, coverageDescriptor.one);
248
+ const row = findFragmentSeedRow(coverageAst, raw, ref.table, ref.pk, (table) => ctx.store.primaryKeyFor(table));
249
+ return projection.project(row);
250
+ }
251
+ function findFragmentSeedRow(ast, raw, table, pk, primaryKeyFor) {
252
+ if (Array.isArray(raw)) {
253
+ for (const row of raw) {
254
+ const found = findFragmentSeedRowInObject(ast, row, table, pk, primaryKeyFor);
255
+ if (found !== null)
256
+ return found;
257
+ }
258
+ return null;
259
+ }
260
+ return findFragmentSeedRowInObject(ast, raw, table, pk, primaryKeyFor);
261
+ }
262
+ function findFragmentSeedRowInObject(ast, raw, table, pk, primaryKeyFor) {
263
+ if (raw === null || raw === undefined || typeof raw !== "object")
264
+ return null;
265
+ const row = raw;
266
+ if (ast.table === table && rowMatchesPk(row, pk, primaryKeyFor(table)))
267
+ return row;
268
+ for (const rel of ast.related ?? []) {
269
+ if (rel.subquery.aggregate !== undefined)
270
+ continue;
271
+ const alias = rel.subquery.alias;
272
+ if (alias === undefined)
273
+ continue;
274
+ const found = findFragmentSeedRow(rel.subquery, row[alias], table, pk, primaryKeyFor);
275
+ if (found !== null)
276
+ return found;
277
+ }
278
+ return null;
279
+ }
280
+ function rowMatchesPk(row, pk, primaryKey) {
281
+ for (const col of primaryKey) {
282
+ if (!Object.prototype.hasOwnProperty.call(row, col))
283
+ return false;
284
+ if (stableKey(row[col]) !== stableKey(pk[col]))
285
+ return false;
286
+ }
287
+ return true;
288
+ }
289
+ class LocalFragmentProjection {
290
+ manyCache = new WeakMap();
291
+ cache = new WeakMap();
292
+ refCache = new Map();
293
+ ast;
294
+ coverage;
295
+ primaryKeyFor;
296
+ constructor(ast, coverage, primaryKeyFor) {
297
+ this.ast = ast;
298
+ this.coverage = coverage;
299
+ this.primaryKeyFor = primaryKeyFor;
300
+ }
301
+ projectOne(raw) {
302
+ return this.project(raw);
303
+ }
304
+ projectMany(raw) {
305
+ if (!Array.isArray(raw))
306
+ return EMPTY_ARRAY;
307
+ const cached = this.manyCache.get(raw);
308
+ if (cached)
309
+ return cached;
310
+ const out = raw.map((row) => this.project(row));
311
+ this.manyCache.set(raw, out);
312
+ return out;
313
+ }
314
+ project(raw) {
315
+ if (raw === null || raw === undefined || typeof raw !== "object")
316
+ return null;
317
+ const cached = this.cache.get(raw);
318
+ if (cached !== undefined)
319
+ return cached;
320
+ const out = this.projectRow(this.ast, raw);
321
+ this.cache.set(raw, out);
322
+ return out;
323
+ }
324
+ projectRow(ast, raw) {
325
+ if (raw === null || raw === undefined || typeof raw !== "object")
326
+ return null;
327
+ const row = raw;
328
+ const out = {};
329
+ const relationshipAliases = new Set((ast.related ?? []).map((rel) => rel.subquery.alias).filter((a) => a !== undefined));
330
+ if (ast.select === undefined) {
331
+ for (const [key, value] of Object.entries(row)) {
332
+ if (!relationshipAliases.has(key))
333
+ out[key] = value;
334
+ }
335
+ }
336
+ else {
337
+ for (const col of ast.select) {
338
+ if (Object.prototype.hasOwnProperty.call(row, col))
339
+ out[col] = row[col];
340
+ }
341
+ }
342
+ for (const rel of ast.related ?? []) {
343
+ const alias = rel.subquery.alias;
344
+ if (alias === undefined)
345
+ continue;
346
+ const value = row[alias];
347
+ if (rel.subquery.aggregate !== undefined) {
348
+ out[alias] = value;
349
+ continue;
350
+ }
351
+ out[alias] = isFragmentRelationship(rel) ? this.projectRelatedRefs(rel.subquery, value) : this.projectInline(rel.subquery, value);
352
+ }
353
+ return out;
354
+ }
355
+ projectInline(childAst, value) {
356
+ if (Array.isArray(value))
357
+ return value.map((row) => this.projectRow(childAst, row));
358
+ if (value === null || value === undefined)
359
+ return value ?? null;
360
+ return this.projectRow(childAst, value);
361
+ }
362
+ projectRelatedRefs(childAst, value) {
363
+ if (Array.isArray(value))
364
+ return value.map((row) => this.refForRow(childAst.table, row));
365
+ if (value === null || value === undefined)
366
+ return value ?? null;
367
+ return this.refForRow(childAst.table, value);
368
+ }
369
+ refForRow(table, value) {
370
+ if (value === null || typeof value !== "object") {
371
+ throw new Error(`useFragment(): cannot build a local fragment ref for "${table}" from a non-object row.`);
372
+ }
373
+ const row = value;
374
+ const pk = {};
375
+ for (const col of this.primaryKeyFor(table)) {
376
+ if (!Object.prototype.hasOwnProperty.call(row, col)) {
377
+ throw new Error(`useFragment(): local relationship row for "${table}" is missing primary key column "${col}".`);
378
+ }
379
+ pk[col] = row[col];
380
+ }
381
+ const key = stableKey({ table, pk });
382
+ const cached = this.refCache.get(key);
383
+ if (cached)
384
+ return cached;
385
+ const ref = createLocalFragmentRefForTable(table, pk, this.coverage);
386
+ this.refCache.set(key, ref);
387
+ return ref;
388
+ }
389
+ }
390
+ class RootRefProjection {
391
+ cache = new WeakMap();
392
+ rowCache = new WeakMap();
393
+ keyCache = new Map();
394
+ table;
395
+ coverage;
396
+ primaryKeyFor;
397
+ constructor(fragment, coverage, primaryKeyFor) {
398
+ this.table = tableMeta(fragment.table).name;
399
+ this.coverage = coverage;
400
+ this.primaryKeyFor = primaryKeyFor;
401
+ }
402
+ projectOne(raw) {
403
+ if (raw === null || raw === undefined)
404
+ return null;
405
+ return this.refForRow(raw);
406
+ }
407
+ projectMany(raw) {
408
+ if (!Array.isArray(raw))
409
+ return EMPTY_ARRAY;
410
+ const cached = this.cache.get(raw);
411
+ if (cached)
412
+ return cached;
413
+ const refs = raw.map((row) => this.refForRow(row));
414
+ this.cache.set(raw, refs);
415
+ return refs;
416
+ }
417
+ refForRow(value) {
418
+ if (value === null || typeof value !== "object") {
419
+ throw new Error(`useRoot(): cannot build a fragment ref for "${this.table}" from a non-object row.`);
420
+ }
421
+ const cached = this.rowCache.get(value);
422
+ if (cached)
423
+ return cached;
424
+ const row = value;
425
+ const pk = {};
426
+ for (const col of this.primaryKeyFor(this.table)) {
427
+ if (!Object.prototype.hasOwnProperty.call(row, col)) {
428
+ throw new Error(`useRoot(): local root row for "${this.table}" is missing primary key column "${col}".`);
429
+ }
430
+ pk[col] = row[col];
431
+ }
432
+ const key = stableKey({ table: this.table, pk });
433
+ const existing = this.keyCache.get(key);
434
+ if (existing) {
435
+ this.rowCache.set(value, existing);
436
+ return existing;
437
+ }
438
+ const ref = createLocalFragmentRefForTable(this.table, pk, this.coverage);
439
+ this.keyCache.set(key, ref);
440
+ this.rowCache.set(value, ref);
441
+ return ref;
442
+ }
443
+ }
444
+ export class SyncQueryCache {
445
+ entries = new Map();
446
+ nextLeaseId = 1;
447
+ store;
448
+ constructor(store) {
449
+ this.store = store;
450
+ }
451
+ retain(coverageKey, query) {
452
+ let entry = this.entries.get(coverageKey);
453
+ if (!entry) {
454
+ const handle = this.createHandle(query);
455
+ entry = {
456
+ handle,
457
+ leases: [],
458
+ listeners: new Set(),
459
+ unsubscribe: () => { },
460
+ };
461
+ entry.unsubscribe = handle.subscribe(() => {
462
+ for (const listener of entry.listeners)
463
+ listener();
464
+ });
465
+ this.entries.set(coverageKey, entry);
466
+ }
467
+ const lease = { id: this.nextLeaseId++, coverageKey };
468
+ entry.leases.push(lease);
469
+ return lease;
470
+ }
471
+ release(lease) {
472
+ const entry = this.entries.get(lease.coverageKey);
473
+ if (!entry)
474
+ return;
475
+ const index = entry.leases.findIndex((l) => l.id === lease.id);
476
+ if (index < 0)
477
+ return;
478
+ entry.leases.splice(index, 1);
479
+ if (entry.leases.length > 0)
480
+ return;
481
+ entry.unsubscribe();
482
+ entry.handle.release();
483
+ this.entries.delete(lease.coverageKey);
484
+ }
485
+ subscribe(coverageKey, listener) {
486
+ const entry = this.entries.get(coverageKey);
487
+ if (!entry)
488
+ return () => { };
489
+ entry.listeners.add(listener);
490
+ return () => {
491
+ entry.listeners.delete(listener);
492
+ };
493
+ }
494
+ resultType(coverageKey) {
495
+ return this.entries.get(coverageKey)?.handle.resultType ?? "unknown";
496
+ }
497
+ size() {
498
+ return this.entries.size;
499
+ }
500
+ createHandle(query) {
501
+ if (this.store.canRetainRemoteQueries())
502
+ return this.store.retainSyncQuery(query);
503
+ const view = this.store.materialize(query);
504
+ return {
505
+ get resultType() {
506
+ return view.resultType;
507
+ },
508
+ subscribe: (listener) => view.subscribe(listener),
509
+ release: () => view.destroy(),
510
+ };
511
+ }
83
512
  }
84
513
  export class QueryCache {
85
514
  entries = new Map();