@polyphron/viper-react 0.1.2 → 0.3.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 +225 -1
- package/dist/auth.d.ts +45 -4
- package/dist/auth.js +178 -4
- package/dist/db-fetch.d.ts +121 -0
- package/dist/db-fetch.js +426 -0
- package/dist/db-page.d.ts +74 -0
- package/dist/db-page.js +233 -0
- package/dist/db-query.d.ts +44 -0
- package/dist/db-query.js +172 -0
- package/dist/db.d.ts +70 -0
- package/dist/db.js +329 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/page-plan.d.ts +29 -0
- package/dist/page-plan.js +102 -0
- package/dist/records.d.ts +7 -1
- package/dist/records.js +26 -18
- package/dist/runs.d.ts +23 -0
- package/dist/runs.js +58 -0
- package/package.json +20 -4
package/dist/db-fetch.js
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// EXPERIMENTAL: any fetch as a TanStack DB collection.
|
|
2
|
+
//
|
|
3
|
+
// For data that isn't records: settings, keys, logs, anything an endpoint
|
|
4
|
+
// hands back whole. It loads with `fetch`, loads again when told to (or on a
|
|
5
|
+
// timer, or when the window comes back and it's stale), and keeps the order
|
|
6
|
+
// the server gave. Records have realtime, so use `viperCollectionOptions` or
|
|
7
|
+
// `useLivePage` for them.
|
|
8
|
+
import { ViperError } from "@polyphron/viper-client";
|
|
9
|
+
import { createCollection } from "@tanstack/db";
|
|
10
|
+
import { useLiveQuery } from "@tanstack/react-db";
|
|
11
|
+
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
12
|
+
// The key a one-object fetch keeps its row under.
|
|
13
|
+
const ONE = "one";
|
|
14
|
+
// Loads that failed on the network (not "no" or "not found") try again.
|
|
15
|
+
const RETRIES = 2;
|
|
16
|
+
// Every fetched collection that has a reader, for the focus check.
|
|
17
|
+
const watched = new Set();
|
|
18
|
+
if (typeof window !== "undefined") {
|
|
19
|
+
window.addEventListener("focus", () => watched.forEach((entry) => entry.onFocus()));
|
|
20
|
+
}
|
|
21
|
+
// True when two rows hold the same values.
|
|
22
|
+
function same(a, b) {
|
|
23
|
+
return a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
24
|
+
}
|
|
25
|
+
// Waits `ms`, or less if `signal` aborts.
|
|
26
|
+
function wait(ms, signal) {
|
|
27
|
+
return new Promise((done) => {
|
|
28
|
+
const timer = setTimeout(done, ms);
|
|
29
|
+
signal.addEventListener("abort", () => {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
done();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Options for `createCollection()`: what `fetch` gives, as rows. A list is
|
|
37
|
+
* keyed by `getKey`; a single object (leave `getKey` out) is one row.
|
|
38
|
+
*/
|
|
39
|
+
export function fetchCollectionOptions(options) {
|
|
40
|
+
const { key, fetch, getKey, refetchInterval } = options;
|
|
41
|
+
const staleTime = options.staleTime ?? 30_000;
|
|
42
|
+
const kind = getKey ? "list" : "one";
|
|
43
|
+
const keyOf = getKey ?? (() => ONE);
|
|
44
|
+
const toRows = (data) => kind === "list" ? (data ?? []) : data ? [data] : [];
|
|
45
|
+
let state = {
|
|
46
|
+
order: [],
|
|
47
|
+
error: null,
|
|
48
|
+
loadedAt: 0,
|
|
49
|
+
fetching: false,
|
|
50
|
+
};
|
|
51
|
+
let last;
|
|
52
|
+
let readers = 0;
|
|
53
|
+
// Set by stale(), cleared by the next load.
|
|
54
|
+
let marked = false;
|
|
55
|
+
// Bumped by set(), so a load that started before it can't undo it.
|
|
56
|
+
let generation = 0;
|
|
57
|
+
const listeners = new Set();
|
|
58
|
+
const waiting = new Set();
|
|
59
|
+
const publish = (next) => {
|
|
60
|
+
state = { ...state, ...next };
|
|
61
|
+
listeners.forEach((listener) => listener());
|
|
62
|
+
};
|
|
63
|
+
// Set while the sync runs.
|
|
64
|
+
let live = null;
|
|
65
|
+
const sync = (params) => {
|
|
66
|
+
const { begin, write, commit, markReady } = params;
|
|
67
|
+
const rows = new Map();
|
|
68
|
+
let closed = false;
|
|
69
|
+
let loading = null;
|
|
70
|
+
let again = false;
|
|
71
|
+
let timer;
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
// Writes only what changed, so rows that stayed the same keep their
|
|
74
|
+
// identity and whatever renders them doesn't start over.
|
|
75
|
+
const apply = (data) => {
|
|
76
|
+
const next = toRows(data);
|
|
77
|
+
const keys = next.map(keyOf);
|
|
78
|
+
const kept = new Set(keys);
|
|
79
|
+
last = data;
|
|
80
|
+
marked = false;
|
|
81
|
+
begin();
|
|
82
|
+
for (const [k] of rows) {
|
|
83
|
+
if (!kept.has(k)) {
|
|
84
|
+
write({ type: "delete", key: k });
|
|
85
|
+
rows.delete(k);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
next.forEach((row, i) => {
|
|
89
|
+
const k = keys[i];
|
|
90
|
+
const old = rows.get(k);
|
|
91
|
+
if (old && same(old, row))
|
|
92
|
+
return;
|
|
93
|
+
write({ type: old ? "update" : "insert", value: row });
|
|
94
|
+
rows.set(k, row);
|
|
95
|
+
});
|
|
96
|
+
commit();
|
|
97
|
+
markReady();
|
|
98
|
+
publish({
|
|
99
|
+
order: same(keys, state.order) ? state.order : keys,
|
|
100
|
+
error: null,
|
|
101
|
+
loadedAt: Date.now(),
|
|
102
|
+
});
|
|
103
|
+
waiting.forEach((w) => w.done());
|
|
104
|
+
waiting.clear();
|
|
105
|
+
};
|
|
106
|
+
// The next timed load, while someone reads it.
|
|
107
|
+
const schedule = () => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
const every = typeof refetchInterval === "function"
|
|
110
|
+
? last === undefined
|
|
111
|
+
? false
|
|
112
|
+
: refetchInterval(last)
|
|
113
|
+
: refetchInterval;
|
|
114
|
+
if (every && readers > 0 && !closed) {
|
|
115
|
+
// A hidden tab waits rather than asking; nobody is looking.
|
|
116
|
+
timer = setTimeout(() => typeof document !== "undefined" && document.hidden
|
|
117
|
+
? schedule()
|
|
118
|
+
: void load(), every);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const load = async () => {
|
|
122
|
+
// One load at a time; a call during one loads again after it.
|
|
123
|
+
if (loading) {
|
|
124
|
+
again = true;
|
|
125
|
+
return loading;
|
|
126
|
+
}
|
|
127
|
+
loading = (async () => {
|
|
128
|
+
publish({ fetching: true });
|
|
129
|
+
try {
|
|
130
|
+
do {
|
|
131
|
+
again = false;
|
|
132
|
+
let data;
|
|
133
|
+
const at = generation;
|
|
134
|
+
for (let tries = 0;; tries++) {
|
|
135
|
+
try {
|
|
136
|
+
data = await fetch(controller.signal);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
// The server said no. No answer at all (status 0) and
|
|
141
|
+
// server trouble are worth another go.
|
|
142
|
+
const refused = error instanceof ViperError &&
|
|
143
|
+
error.status > 0 &&
|
|
144
|
+
error.status < 500;
|
|
145
|
+
if (closed || refused || tries >= RETRIES)
|
|
146
|
+
throw error;
|
|
147
|
+
await wait(1000 * 2 ** tries, controller.signal);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (closed)
|
|
151
|
+
return;
|
|
152
|
+
// set() put newer data in while this was on the way.
|
|
153
|
+
if (at === generation)
|
|
154
|
+
apply(data);
|
|
155
|
+
} while (again && !closed);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (closed)
|
|
159
|
+
return;
|
|
160
|
+
// Ready with no rows, so preload() settles; the error is in state.
|
|
161
|
+
markReady();
|
|
162
|
+
publish({ error });
|
|
163
|
+
waiting.forEach((w) => w.fail(error));
|
|
164
|
+
waiting.clear();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
loading = null;
|
|
168
|
+
publish({ fetching: false });
|
|
169
|
+
schedule();
|
|
170
|
+
}
|
|
171
|
+
})();
|
|
172
|
+
return loading;
|
|
173
|
+
};
|
|
174
|
+
live = { load, apply, schedule };
|
|
175
|
+
void load();
|
|
176
|
+
return () => {
|
|
177
|
+
closed = true;
|
|
178
|
+
live = null;
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
controller.abort();
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
// True when a reader should ask again: an old copy, or a failed first
|
|
184
|
+
// load (it may have been a blip).
|
|
185
|
+
const due = () => !state.fetching &&
|
|
186
|
+
(state.loadedAt
|
|
187
|
+
? marked || Date.now() - state.loadedAt > staleTime
|
|
188
|
+
: !!state.error);
|
|
189
|
+
const entry = {
|
|
190
|
+
onFocus: () => {
|
|
191
|
+
if (options.refetchOnFocus !== false && due())
|
|
192
|
+
void live?.load();
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const utils = {
|
|
196
|
+
state: () => state,
|
|
197
|
+
subscribe: (listener) => {
|
|
198
|
+
listeners.add(listener);
|
|
199
|
+
return () => listeners.delete(listener);
|
|
200
|
+
},
|
|
201
|
+
refetch: async () => live?.load(),
|
|
202
|
+
ready: () => {
|
|
203
|
+
if (state.loadedAt)
|
|
204
|
+
return Promise.resolve();
|
|
205
|
+
// A failed first load is asked again, not handed back as is.
|
|
206
|
+
if (due())
|
|
207
|
+
void live?.load();
|
|
208
|
+
return new Promise((done, fail) => waiting.add({ done, fail }));
|
|
209
|
+
},
|
|
210
|
+
set: (data) => {
|
|
211
|
+
generation++;
|
|
212
|
+
live?.apply(data);
|
|
213
|
+
},
|
|
214
|
+
watch: () => {
|
|
215
|
+
readers++;
|
|
216
|
+
watched.add(entry);
|
|
217
|
+
// A reader that comes back to an old copy asks again.
|
|
218
|
+
if (due()) {
|
|
219
|
+
void live?.load();
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
live?.schedule();
|
|
223
|
+
}
|
|
224
|
+
return () => {
|
|
225
|
+
readers--;
|
|
226
|
+
if (readers === 0)
|
|
227
|
+
watched.delete(entry);
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
stale: async () => {
|
|
231
|
+
marked = true;
|
|
232
|
+
if (readers > 0)
|
|
233
|
+
await live?.load();
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
id: `viper-fetch:${JSON.stringify(key)}`,
|
|
238
|
+
getKey: keyOf,
|
|
239
|
+
// An endpoint sends whole rows, so an update replaces rather than merges.
|
|
240
|
+
sync: { sync, rowUpdateMode: "full" },
|
|
241
|
+
utils,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// Fetched collections by owner and key, so every reader of a key shares one
|
|
245
|
+
// collection (and one request). One leaves here when TanStack DB cleans it up,
|
|
246
|
+
// after nothing has read it for a while.
|
|
247
|
+
const made = new WeakMap();
|
|
248
|
+
/**
|
|
249
|
+
* The fetched collection for `options.key` under `owner` (a client, say),
|
|
250
|
+
* made once and shared. For a list pass `getKey`; leave it out for one object.
|
|
251
|
+
*/
|
|
252
|
+
export function fetched(owner, options) {
|
|
253
|
+
let byKey = made.get(owner);
|
|
254
|
+
if (!byKey) {
|
|
255
|
+
byKey = new Map();
|
|
256
|
+
made.set(owner, byKey);
|
|
257
|
+
}
|
|
258
|
+
const hash = JSON.stringify(options.key);
|
|
259
|
+
const found = byKey.get(hash);
|
|
260
|
+
if (found)
|
|
261
|
+
return found;
|
|
262
|
+
const collection = Object.assign(createCollection(fetchCollectionOptions(options)), { kind: options.getKey ? "list" : "one", key: options.key });
|
|
263
|
+
// Asking for the data (or putting some in) starts the sync if nothing
|
|
264
|
+
// has yet, so a route loader can await ready() on its own.
|
|
265
|
+
const { ready, set } = collection.utils;
|
|
266
|
+
collection.utils.ready = () => {
|
|
267
|
+
collection.startSyncImmediate();
|
|
268
|
+
return ready();
|
|
269
|
+
};
|
|
270
|
+
collection.utils.set = (data) => {
|
|
271
|
+
collection.startSyncImmediate();
|
|
272
|
+
set(data);
|
|
273
|
+
};
|
|
274
|
+
collection.on("status:change", ({ status }) => {
|
|
275
|
+
if (status === "cleaned-up")
|
|
276
|
+
byKey.delete(hash);
|
|
277
|
+
});
|
|
278
|
+
byKey.set(hash, collection);
|
|
279
|
+
return collection;
|
|
280
|
+
}
|
|
281
|
+
// True when `key` starts with every part of `prefix`.
|
|
282
|
+
function startsWith(key, prefix) {
|
|
283
|
+
return prefix.every((part, i) => same(part, key[i]));
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Loads again every fetched collection under `owner` whose key starts with
|
|
287
|
+
* `prefix`: the ones on screen now, the rest when next read. Call it after a
|
|
288
|
+
* change, the way you'd invalidate a query.
|
|
289
|
+
*/
|
|
290
|
+
export function refetchFetched(owner, prefix) {
|
|
291
|
+
const loads = [];
|
|
292
|
+
for (const collection of made.get(owner)?.values() ?? []) {
|
|
293
|
+
if (!startsWith(collection.key, prefix))
|
|
294
|
+
continue;
|
|
295
|
+
loads.push(collection.utils.stale());
|
|
296
|
+
}
|
|
297
|
+
return Promise.all(loads).then(() => undefined);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Drops every fetched collection under `owner` whose key starts with
|
|
301
|
+
* `prefix`, so nothing shows its data again (after a project is deleted).
|
|
302
|
+
*/
|
|
303
|
+
export function forgetFetched(owner, prefix) {
|
|
304
|
+
const byKey = made.get(owner);
|
|
305
|
+
for (const [hash, collection] of byKey ?? []) {
|
|
306
|
+
if (!startsWith(collection.key, prefix))
|
|
307
|
+
continue;
|
|
308
|
+
byKey?.delete(hash);
|
|
309
|
+
void collection.cleanup();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const IDLE = {
|
|
313
|
+
order: [],
|
|
314
|
+
error: null,
|
|
315
|
+
loadedAt: 0,
|
|
316
|
+
fetching: false,
|
|
317
|
+
};
|
|
318
|
+
/**
|
|
319
|
+
* A fetched collection read as a list (or its one object), in the server's
|
|
320
|
+
* order. Pass null to read nothing yet. With `keepPrevious`, the last data
|
|
321
|
+
* stays on screen while a new collection (the next page, a new filter) loads.
|
|
322
|
+
*
|
|
323
|
+
* ```tsx
|
|
324
|
+
* const keys = useFetched(fetched(client, { key: ["keys"], fetch, getKey }))
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
327
|
+
export function useFetched(collection, options = {}) {
|
|
328
|
+
const live = useLiveQuery({
|
|
329
|
+
query: (q) => collection
|
|
330
|
+
? q.from({ row: collection })
|
|
331
|
+
: undefined,
|
|
332
|
+
});
|
|
333
|
+
const state = useSyncExternalStore(collection?.utils.subscribe ?? noSubscribe, collection?.utils.state ?? idle, collection?.utils.state ?? idle);
|
|
334
|
+
useEffect(() => collection?.utils.watch(), [collection]);
|
|
335
|
+
// The collection keeps rows by key; the order is the server's.
|
|
336
|
+
const loaded = state.loadedAt > 0;
|
|
337
|
+
const data = useMemo(() => {
|
|
338
|
+
if (!collection || !loaded)
|
|
339
|
+
return undefined;
|
|
340
|
+
return read(collection);
|
|
341
|
+
// live.data changes whenever a row does.
|
|
342
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
343
|
+
}, [collection, live.data, state.order, loaded]);
|
|
344
|
+
// The last data that loaded, shown while the next collection does.
|
|
345
|
+
const [last, setLast] = useState(undefined);
|
|
346
|
+
if (options.keepPrevious && data !== undefined && data !== last)
|
|
347
|
+
setLast(data);
|
|
348
|
+
const error = (state.error ?? null);
|
|
349
|
+
const shown = data ?? (options.keepPrevious && !error ? last : undefined);
|
|
350
|
+
return {
|
|
351
|
+
data: shown,
|
|
352
|
+
error,
|
|
353
|
+
isError: !!error,
|
|
354
|
+
isPending: !!collection && shown === undefined && !error,
|
|
355
|
+
isSuccess: shown !== undefined,
|
|
356
|
+
isFetching: state.fetching,
|
|
357
|
+
updatedAt: state.loadedAt,
|
|
358
|
+
refetch: async () => collection?.utils.refetch(),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Several fetched collections at once (one per project, say), in the same
|
|
363
|
+
* order: `{ data, error }` each, with `data` undefined until it loads.
|
|
364
|
+
*/
|
|
365
|
+
export function useFetchedAll(collections) {
|
|
366
|
+
const [tick, setTick] = useState(0);
|
|
367
|
+
// The list is usually made fresh each render, so compare by keys.
|
|
368
|
+
const id = collections.map((c) => JSON.stringify(c.key)).join(" ");
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
const undo = collections.flatMap((c) => {
|
|
371
|
+
// Re-render only when data or an error lands, not on every fetch flag.
|
|
372
|
+
let seen = c.utils.state();
|
|
373
|
+
// Nothing else reads these, so start each and hold it, or TanStack DB
|
|
374
|
+
// would leave it idle, then clean it up.
|
|
375
|
+
c.startSyncImmediate();
|
|
376
|
+
const held = c.subscribeChanges(() => { });
|
|
377
|
+
return [
|
|
378
|
+
c.utils.watch(),
|
|
379
|
+
() => held.unsubscribe(),
|
|
380
|
+
c.utils.subscribe(() => {
|
|
381
|
+
const now = c.utils.state();
|
|
382
|
+
if (now.loadedAt !== seen.loadedAt || now.error !== seen.error) {
|
|
383
|
+
setTick((n) => n + 1);
|
|
384
|
+
}
|
|
385
|
+
seen = now;
|
|
386
|
+
}),
|
|
387
|
+
];
|
|
388
|
+
});
|
|
389
|
+
return () => undo.forEach((u) => u());
|
|
390
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
391
|
+
}, [id]);
|
|
392
|
+
return useMemo(() => collections.map((c) => {
|
|
393
|
+
const state = c.utils.state();
|
|
394
|
+
const data = state.loadedAt ? read(c) : undefined;
|
|
395
|
+
return { data, error: (state.error ?? null) };
|
|
396
|
+
}),
|
|
397
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
398
|
+
[id, tick]);
|
|
399
|
+
}
|
|
400
|
+
// A row without the `$synced`, `$key`... fields TanStack DB adds, so a
|
|
401
|
+
// form can send it back as is. Cached, so a row keeps its identity.
|
|
402
|
+
const plainRows = new WeakMap();
|
|
403
|
+
const VIRTUAL = ["$synced", "$origin", "$key", "$collectionId"];
|
|
404
|
+
function plain(row) {
|
|
405
|
+
let found = plainRows.get(row);
|
|
406
|
+
if (!found) {
|
|
407
|
+
const copy = {
|
|
408
|
+
...row,
|
|
409
|
+
};
|
|
410
|
+
VIRTUAL.forEach((name) => delete copy[name]);
|
|
411
|
+
found = copy;
|
|
412
|
+
plainRows.set(row, found);
|
|
413
|
+
}
|
|
414
|
+
return found;
|
|
415
|
+
}
|
|
416
|
+
// The rows of `collection` in the server's order, as plain objects.
|
|
417
|
+
function read(collection) {
|
|
418
|
+
const rows = collection.utils.state().order.flatMap((k) => {
|
|
419
|
+
const row = collection.get(k);
|
|
420
|
+
return row ? [plain(row)] : [];
|
|
421
|
+
});
|
|
422
|
+
return (collection.kind === "one" ? rows[0] : rows);
|
|
423
|
+
}
|
|
424
|
+
// For a null collection: nothing to hear, nothing loaded.
|
|
425
|
+
const noSubscribe = () => () => { };
|
|
426
|
+
const idle = () => IDLE;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ListOptions, RealtimeEvent, RecordModel, Viper } from "@polyphron/viper-client";
|
|
2
|
+
import type { Collection, CollectionConfig } from "@tanstack/db";
|
|
3
|
+
/** Which page: what `records.list` takes, less the count-free options. */
|
|
4
|
+
export type PageQuery = Omit<ListOptions, "skipTotal" | "cursor" | "signal">;
|
|
5
|
+
/** A page's totals and the order its rows go in. */
|
|
6
|
+
export type PageInfo = {
|
|
7
|
+
/** The row ids, in the server's order. */
|
|
8
|
+
order: string[];
|
|
9
|
+
page: number;
|
|
10
|
+
perPage: number;
|
|
11
|
+
totalItems: number;
|
|
12
|
+
totalPages: number;
|
|
13
|
+
};
|
|
14
|
+
/** What a page collection adds under `collection.utils`. */
|
|
15
|
+
export type PageUtils = {
|
|
16
|
+
/** The totals and the order now. */
|
|
17
|
+
info: () => PageInfo;
|
|
18
|
+
/** Calls `listener` whenever `info()` changes. Returns the unsubscribe. */
|
|
19
|
+
subscribe: (listener: () => void) => () => void;
|
|
20
|
+
/** Asks the server for the page again. */
|
|
21
|
+
refetch: () => Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Shows a change made outside the collection (a save with files, a
|
|
24
|
+
* restore) the way a realtime event would, without waiting for one.
|
|
25
|
+
*/
|
|
26
|
+
apply: (event: RealtimeEvent) => void;
|
|
27
|
+
};
|
|
28
|
+
export type ViperPageOptions = {
|
|
29
|
+
viper: Viper;
|
|
30
|
+
/** The Viper collection's name, e.g. `posts`. */
|
|
31
|
+
collection: string;
|
|
32
|
+
query: PageQuery;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Options for `createCollection()`: one server page of `collection`, kept
|
|
36
|
+
* fresh from realtime. Its totals and row order are under `utils.info()`.
|
|
37
|
+
* Deleting rows from it hides them at once, sends one all-or-nothing batch,
|
|
38
|
+
* and puts them back if the server says no.
|
|
39
|
+
*/
|
|
40
|
+
export declare function viperPageOptions<T extends RecordModel = RecordModel>(options: ViperPageOptions): CollectionConfig<T, string, never, PageUtils> & {
|
|
41
|
+
utils: PageUtils;
|
|
42
|
+
};
|
|
43
|
+
/** A page collection, as `viperPage` and `useLivePage` give it. */
|
|
44
|
+
export type PageCollection<T extends RecordModel = RecordModel> = Collection<T, string, PageUtils>;
|
|
45
|
+
/**
|
|
46
|
+
* The page collection for `collection` and `query` on `viper`, made once and
|
|
47
|
+
* shared. Outside React (a route loader, say) call `.preload()` on it to
|
|
48
|
+
* start the fetch early.
|
|
49
|
+
*/
|
|
50
|
+
export declare function viperPage<T extends RecordModel = RecordModel>(viper: Viper, collection: string, query: PageQuery): PageCollection<T>;
|
|
51
|
+
/** What `useLivePage` gives back. */
|
|
52
|
+
export type LivePage<T extends RecordModel = RecordModel> = {
|
|
53
|
+
/** The page's rows in order, or null before the first load. */
|
|
54
|
+
items: T[] | null;
|
|
55
|
+
totalItems: number;
|
|
56
|
+
totalPages: number;
|
|
57
|
+
/** True while a page loads, including the next one after a change of query. */
|
|
58
|
+
loading: boolean;
|
|
59
|
+
error: unknown;
|
|
60
|
+
/** The collection, to delete rows or call `utils.refetch()` and `utils.apply()`. */
|
|
61
|
+
collection: PageCollection<T>;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* One server page of `collection` as a live list: `records.list` once, then
|
|
65
|
+
* realtime keeps it fresh, patching rows where it can. While a new `query`
|
|
66
|
+
* loads, the last page stays on screen.
|
|
67
|
+
*
|
|
68
|
+
* ```tsx
|
|
69
|
+
* const { items, totalPages } = useLivePage<Post>("posts", {
|
|
70
|
+
* page, perPage: 30, sort: "-updated", filter,
|
|
71
|
+
* })
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function useLivePage<T extends RecordModel = RecordModel>(collection: string, query: PageQuery): LivePage<T>;
|