@polyphron/viper-react 0.3.0 → 0.4.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/dist/db-page.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createCollection } from "@tanstack/db";
2
2
  import { useLiveQuery } from "@tanstack/react-db";
3
3
  import { useMemo, useState, useSyncExternalStore } from "react";
4
+ import { cacheOf, sendOps, singleFlight } from "./db-shared.js";
4
5
  import { pageComparator, planPage } from "./page-plan.js";
5
6
  import { useViper } from "./provider.js";
6
7
  // A burst of changes (an import, a batch) asks for the page once.
@@ -12,14 +13,6 @@ const EMPTY = {
12
13
  totalItems: 0,
13
14
  totalPages: 0,
14
15
  };
15
- // A delete of several rows, as one all-or-nothing batch.
16
- async function deleteAll(viper, collection, ids) {
17
- if (ids.length === 1) {
18
- await viper.collection(collection).records.delete(ids[0]);
19
- return;
20
- }
21
- await viper.batch(ids.map((id) => ({ method: "delete", collection, id })));
22
- }
23
16
  /**
24
17
  * Options for `createCollection()`: one server page of `collection`, kept
25
18
  * fresh from realtime. Its totals and row order are under `utils.info()`.
@@ -42,59 +35,42 @@ export function viperPageOptions(options) {
42
35
  const sync = (params) => {
43
36
  const { begin, write, commit, truncate, markReady, markError } = params;
44
37
  let closed = false;
45
- let loading = null;
46
- let again = false;
47
38
  let timer;
48
- const load = async () => {
49
- // One load at a time; a change during a load reloads after it.
50
- if (loading) {
51
- again = true;
52
- return loading;
53
- }
54
- loading = (async () => {
55
- try {
56
- do {
57
- again = false;
58
- const result = await records.list(query);
59
- if (closed)
60
- return;
61
- // Truncate and rewrite in one transaction, so a reload never
62
- // shows an empty page in between.
63
- begin();
64
- truncate();
65
- rows.clear();
66
- for (const record of result.items) {
67
- write({ type: "insert", value: record });
68
- rows.set(record.id, record);
69
- }
70
- commit();
71
- markReady();
72
- publish({
73
- order: result.items.map((r) => r.id),
74
- page: result.page,
75
- perPage: result.perPage,
76
- totalItems: result.totalItems,
77
- totalPages: result.totalPages,
78
- });
79
- } while (again && !closed);
80
- }
81
- catch (error) {
82
- markError(error);
83
- }
84
- finally {
85
- loading = null;
39
+ const load = singleFlight({
40
+ pass: async () => {
41
+ const result = await records.list(query);
42
+ if (closed)
43
+ return;
44
+ // Truncate and rewrite in one transaction, so a reload never
45
+ // shows an empty page in between.
46
+ begin();
47
+ truncate();
48
+ rows.clear();
49
+ for (const record of result.items) {
50
+ write({ type: "insert", value: record });
51
+ rows.set(record.id, record);
86
52
  }
87
- })();
88
- return loading;
89
- };
53
+ commit();
54
+ markReady();
55
+ publish({
56
+ order: result.items.map((r) => r.id),
57
+ page: result.page,
58
+ perPage: result.perPage,
59
+ totalItems: result.totalItems,
60
+ totalPages: result.totalPages,
61
+ });
62
+ },
63
+ closed: () => closed,
64
+ fail: markError,
65
+ });
90
66
  const loadSoon = () => {
91
67
  clearTimeout(timer);
92
68
  timer = setTimeout(() => void load(), SETTLE);
93
69
  };
94
70
  const take = (event) => {
95
71
  // The page on screen may be about to change; plan against the next one.
96
- if (loading) {
97
- again = true;
72
+ if (load.busy()) {
73
+ void load();
98
74
  return;
99
75
  }
100
76
  const plan = planPage(event, {
@@ -158,7 +134,11 @@ export function viperPageOptions(options) {
158
134
  sync: { sync, rowUpdateMode: "full" },
159
135
  utils,
160
136
  onDelete: async ({ transaction }) => {
161
- await deleteAll(viper, collection, transaction.mutations.map((m) => String(m.key)));
137
+ await sendOps(viper, transaction.mutations.map((m) => ({
138
+ method: "delete",
139
+ collection,
140
+ id: String(m.key),
141
+ })));
162
142
  // The rows after them move up.
163
143
  await live?.load();
164
144
  },
@@ -166,32 +146,16 @@ export function viperPageOptions(options) {
166
146
  }
167
147
  // Page collections by client and page, so the route that preloads a page and
168
148
  // the table that shows it share one collection (and one fetch). One leaves
169
- // here when its sync stops, after nothing has read it for a while.
170
- const pages = new WeakMap();
149
+ // the cache when its sync stops, after nothing has read it for a while, and
150
+ // the next ask makes a fresh one.
151
+ const pages = cacheOf();
171
152
  /**
172
153
  * The page collection for `collection` and `query` on `viper`, made once and
173
154
  * shared. Outside React (a route loader, say) call `.preload()` on it to
174
155
  * start the fetch early.
175
156
  */
176
157
  export function viperPage(viper, collection, query) {
177
- let byKey = pages.get(viper);
178
- if (!byKey) {
179
- byKey = new Map();
180
- pages.set(viper, byKey);
181
- }
182
- const key = `${collection}:${JSON.stringify(query)}`;
183
- const found = byKey.get(key);
184
- if (found)
185
- return found;
186
- const made = createCollection(viperPageOptions({ viper, collection, query }));
187
- // Once it is cleaned up it can't be read again, so the next ask makes a
188
- // new one.
189
- made.on("status:change", ({ status }) => {
190
- if (status === "cleaned-up")
191
- byKey.delete(key);
192
- });
193
- byKey.set(key, made);
194
- return made;
158
+ return pages(viper, `${collection}:${JSON.stringify(query)}`, () => createCollection(viperPageOptions({ viper, collection, query })));
195
159
  }
196
160
  /**
197
161
  * One server page of `collection` as a live list: `records.list` once, then
@@ -6,12 +6,6 @@ export type Translated = {
6
6
  /** True when the filter matches exactly what the query does. */
7
7
  exact: boolean;
8
8
  };
9
- /**
10
- * A string as a Viper string literal. Backslash escapes the next character
11
- * in Viper's lexer, so escaping `\` and `"` is enough to keep a value from
12
- * ever ending its own quotes.
13
- */
14
- export declare function quote(value: string): string;
15
9
  /**
16
10
  * A subset's `where` as a Viper filter. Anything Viper cannot say is left out
17
11
  * (so more rows load), and the query filters again in the browser.
package/dist/db-query.js CHANGED
@@ -1,3 +1,11 @@
1
+ // EXPERIMENTAL: turns a TanStack DB subset request (`where`, `orderBy`) into
2
+ // the `filter` and `sort` strings of Viper's records API.
3
+ //
4
+ // Everything here may load MORE than the query asks for, never less: TanStack
5
+ // DB runs every live query's own `where` again over what is loaded, so a
6
+ // wider load only costs bandwidth. `exact` says whether the load is the
7
+ // query's exact answer; only then is it safe to ask for a limited window.
8
+ import { quote } from "@polyphron/viper-client";
1
9
  /** A field name is a plain word, so it can go into a filter unquoted. */
2
10
  const FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
3
11
  /** The comparisons Viper writes the same way TanStack DB names them. */
@@ -17,14 +25,6 @@ const FLIP = {
17
25
  lte: "gte",
18
26
  };
19
27
  const ANY = { text: null, exact: false };
20
- /**
21
- * A string as a Viper string literal. Backslash escapes the next character
22
- * in Viper's lexer, so escaping `\` and `"` is enough to keep a value from
23
- * ever ending its own quotes.
24
- */
25
- export function quote(value) {
26
- return `"${value.replace(/[\\"]/g, "\\$&")}"`;
27
- }
28
28
  // A value as a Viper literal, or null for one Viper has no way to write.
29
29
  function literal(value) {
30
30
  if (typeof value === "string")
@@ -0,0 +1,50 @@
1
+ import type { BatchOp, RecordModel, Viper } from "@polyphron/viper-client";
2
+ /** What `singleFlight` needs from an adapter. */
3
+ export type FlightOptions = {
4
+ /** One load: fetch and put the answer into the collection. */
5
+ pass: () => Promise<void>;
6
+ /** True once the sync has stopped; a stopped sync drops its answers. */
7
+ closed: () => boolean;
8
+ /** Called with whatever a pass threw (the pass is not retried here). */
9
+ fail: (error: unknown) => void;
10
+ /** Runs before the first pass of a flight. */
11
+ start?: () => void;
12
+ /** Runs after a flight ends, whether it worked or failed. */
13
+ settle?: () => void;
14
+ };
15
+ /** A `load()` that runs one pass at a time; `busy()` says whether one runs. */
16
+ export type Flight = (() => Promise<void>) & {
17
+ busy: () => boolean;
18
+ };
19
+ /**
20
+ * Makes a `load()` that runs one pass at a time. A call during a pass does
21
+ * not start a second one; it asks for another pass right after the first, so
22
+ * a change that landed mid-load is never missed. Every caller gets the
23
+ * promise of the flight it joined.
24
+ */
25
+ export declare function singleFlight(options: FlightOptions): Flight;
26
+ /** The bits of a TanStack DB collection that `cacheOf` listens to. */
27
+ type Evictable = {
28
+ on: (event: "status:change", callback: (change: {
29
+ status: string;
30
+ }) => void) => unknown;
31
+ };
32
+ /** A cache of collections; `of(owner)` lists what an owner holds now. */
33
+ export type Cache<V> = ((owner: object, key: string, make: () => V) => V) & {
34
+ of: (owner: object) => Map<string, V> | undefined;
35
+ };
36
+ /**
37
+ * A cache of collections by owner (a client, say) and key, so every reader
38
+ * of a key shares one collection and one request. A collection leaves the
39
+ * cache once TanStack DB cleans it up, after nothing has read it for a while:
40
+ * the next ask builds a fresh one rather than waking the old one.
41
+ */
42
+ export declare function cacheOf<V extends Evictable>(): Cache<V>;
43
+ /**
44
+ * Sends writes: one call for one, a single all-or-nothing batch for several.
45
+ * Resolves to the saved records, in order (null for a delete).
46
+ */
47
+ export declare function sendOps(viper: Viper, ops: BatchOp[]): Promise<(RecordModel | null)[]>;
48
+ /** True when two values are the same, lists and objects included. */
49
+ export declare function same(a: unknown, b: unknown): boolean;
50
+ export {};
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Makes a `load()` that runs one pass at a time. A call during a pass does
3
+ * not start a second one; it asks for another pass right after the first, so
4
+ * a change that landed mid-load is never missed. Every caller gets the
5
+ * promise of the flight it joined.
6
+ */
7
+ export function singleFlight(options) {
8
+ let flight = null;
9
+ let again = false;
10
+ const load = () => {
11
+ if (flight) {
12
+ again = true;
13
+ return flight;
14
+ }
15
+ flight = (async () => {
16
+ options.start?.();
17
+ try {
18
+ do {
19
+ again = false;
20
+ await options.pass();
21
+ } while (again && !options.closed());
22
+ }
23
+ catch (error) {
24
+ options.fail(error);
25
+ }
26
+ finally {
27
+ flight = null;
28
+ options.settle?.();
29
+ }
30
+ })();
31
+ return flight;
32
+ };
33
+ return Object.assign(load, { busy: () => flight !== null });
34
+ }
35
+ /**
36
+ * A cache of collections by owner (a client, say) and key, so every reader
37
+ * of a key shares one collection and one request. A collection leaves the
38
+ * cache once TanStack DB cleans it up, after nothing has read it for a while:
39
+ * the next ask builds a fresh one rather than waking the old one.
40
+ */
41
+ export function cacheOf() {
42
+ const byOwner = new WeakMap();
43
+ const get = (owner, key, make) => {
44
+ let byKey = byOwner.get(owner);
45
+ if (!byKey) {
46
+ byKey = new Map();
47
+ byOwner.set(owner, byKey);
48
+ }
49
+ const found = byKey.get(key);
50
+ if (found)
51
+ return found;
52
+ const made = make();
53
+ const map = byKey;
54
+ made.on("status:change", ({ status }) => {
55
+ if (status === "cleaned-up")
56
+ map.delete(key);
57
+ });
58
+ map.set(key, made);
59
+ return made;
60
+ };
61
+ return Object.assign(get, { of: (owner) => byOwner.get(owner) });
62
+ }
63
+ /**
64
+ * Sends writes: one call for one, a single all-or-nothing batch for several.
65
+ * Resolves to the saved records, in order (null for a delete).
66
+ */
67
+ export async function sendOps(viper, ops) {
68
+ if (ops.length !== 1)
69
+ return viper.batch(ops);
70
+ const op = ops[0];
71
+ const records = viper.collection(op.collection).records;
72
+ if (op.method === "create")
73
+ return [await records.create(op.body)];
74
+ if (op.method === "update")
75
+ return [await records.update(op.id, op.body)];
76
+ await records.delete(op.id);
77
+ return [null];
78
+ }
79
+ /** True when two values are the same, lists and objects included. */
80
+ export function same(a, b) {
81
+ return a === b || JSON.stringify(a) === JSON.stringify(b);
82
+ }
package/dist/db.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createCollection } from "@tanstack/db";
2
2
  import { useLiveQuery } from "@tanstack/react-db";
3
+ import { cacheOf, sendOps, singleFlight } from "./db-shared.js";
3
4
  import { toViperFilter, toViperSort, toWindow } from "./db-query.js";
4
5
  import { useViper } from "./provider.js";
5
6
  export { toViperFilter, toViperSort } from "./db-query.js";
@@ -30,8 +31,6 @@ export function viperCollectionOptions(options) {
30
31
  // event (our own write coming back, say) unable to undo a newer one.
31
32
  const versions = new Map();
32
33
  let closed = false;
33
- let loading = false;
34
- let again = false;
35
34
  // Events that arrive while a load is in flight, replayed after it.
36
35
  let buffered = [];
37
36
  // On-demand: subset loads in flight (they buffer events too), and the
@@ -104,40 +103,27 @@ export function viperCollectionOptions(options) {
104
103
  });
105
104
  return result.items;
106
105
  };
107
- const load = async () => {
108
- // One load at a time; a request during a load runs another after it.
109
- if (loading) {
110
- again = true;
111
- return;
112
- }
113
- loading = true;
114
- try {
115
- do {
116
- again = false;
117
- const items = await fetchAll();
118
- if (closed)
119
- return;
120
- // Truncate and rewrite in one transaction, so a reload never
121
- // shows an empty list in between.
122
- begin();
123
- truncate();
124
- versions.clear();
125
- items.forEach(put);
126
- const missed = buffered;
127
- if (pending === 0)
128
- buffered = [];
129
- missed.forEach(apply);
130
- commit();
131
- markReady();
132
- } while (again);
133
- }
134
- catch (error) {
135
- markError(error);
136
- }
137
- finally {
138
- loading = false;
139
- }
140
- };
106
+ const load = singleFlight({
107
+ pass: async () => {
108
+ const items = await fetchAll();
109
+ if (closed)
110
+ return;
111
+ // Truncate and rewrite in one transaction, so a reload never
112
+ // shows an empty list in between.
113
+ begin();
114
+ truncate();
115
+ versions.clear();
116
+ items.forEach(put);
117
+ const missed = buffered;
118
+ if (pending === 0)
119
+ buffered = [];
120
+ missed.forEach(apply);
121
+ commit();
122
+ markReady();
123
+ },
124
+ closed: () => closed,
125
+ fail: markError,
126
+ });
141
127
  sink = {
142
128
  put,
143
129
  remove,
@@ -152,7 +138,7 @@ export function viperCollectionOptions(options) {
152
138
  if (event.action === "refresh") {
153
139
  void load();
154
140
  }
155
- else if (loading || pending > 0) {
141
+ else if (load.busy() || pending > 0) {
156
142
  buffered.push(event);
157
143
  }
158
144
  else {
@@ -198,7 +184,7 @@ export function viperCollectionOptions(options) {
198
184
  }
199
185
  finally {
200
186
  pending--;
201
- if (pending === 0 && !loading)
187
+ if (pending === 0 && !load.busy())
202
188
  buffered = [];
203
189
  }
204
190
  },
@@ -227,15 +213,15 @@ export function viperCollectionOptions(options) {
227
213
  sync: { sync, rowUpdateMode: "full" },
228
214
  syncMode: onDemand ? "on-demand" : "eager",
229
215
  onInsert: async ({ transaction }) => {
230
- const saved = await save(viper, records, collection, transaction.mutations);
216
+ const saved = await sendOps(viper, toOps(collection, transaction.mutations));
231
217
  store(saved);
232
218
  },
233
219
  onUpdate: async ({ transaction }) => {
234
- const saved = await save(viper, records, collection, transaction.mutations);
220
+ const saved = await sendOps(viper, toOps(collection, transaction.mutations));
235
221
  store(saved);
236
222
  },
237
223
  onDelete: async ({ transaction }) => {
238
- await save(viper, records, collection, transaction.mutations);
224
+ await sendOps(viper, toOps(collection, transaction.mutations));
239
225
  sink?.write(() => {
240
226
  for (const m of transaction.mutations)
241
227
  sink?.remove(m.key);
@@ -248,19 +234,9 @@ export function viperCollectionOptions(options) {
248
234
  function hiddenAnswer(record) {
249
235
  return Object.keys(record).every((key) => key === "id" || key === "collectionId" || key === "collectionName");
250
236
  }
251
- // Sends the mutations: one call for one, a single all-or-nothing batch for
252
- // several. Resolves to the saved records, in order (null for a delete).
253
- async function save(viper, records, collection, mutations) {
254
- if (mutations.length > 1) {
255
- return viper.batch(mutations.map((m) => toOp(collection, m)));
256
- }
257
- const m = mutations[0];
258
- if (m.type === "insert")
259
- return [await records.create(m.modified)];
260
- if (m.type === "update")
261
- return [await records.update(m.key, m.changes)];
262
- await records.delete(m.key);
263
- return [null];
237
+ // The mutations as batch writes; `sendOps` sends one alone when it can.
238
+ function toOps(collection, mutations) {
239
+ return mutations.map((m) => toOp(collection, m));
264
240
  }
265
241
  // One mutation as a batch write.
266
242
  function toOp(collection, m) {
@@ -273,8 +249,9 @@ function toOp(collection, m) {
273
249
  return { method: "delete", collection, id: m.key };
274
250
  }
275
251
  // Collections made by `useViperCollection`, one per client, name and mode, so
276
- // two components asking for `posts` share one copy of it (and one fetch).
277
- const made = new WeakMap();
252
+ // two components asking for `posts` share one copy of it (and one fetch). One
253
+ // leaves when TanStack DB cleans it up, so the next ask gets a fresh one.
254
+ const made = cacheOf();
278
255
  /**
279
256
  * The Viper collection `name` for the client in `<ViperProvider>`, made once
280
257
  * and shared: every component asking for the same name and mode gets the same
@@ -289,18 +266,11 @@ const made = new WeakMap();
289
266
  export function useViperCollection(name, options = {}) {
290
267
  const viper = useViper();
291
268
  const mode = options.syncMode ?? "eager";
292
- let byName = made.get(viper);
293
- if (!byName) {
294
- byName = new Map();
295
- made.set(viper, byName);
296
- }
297
- const key = `${mode}:${name}`;
298
- let found = byName.get(key);
299
- if (!found) {
300
- found = createCollection(viperCollectionOptions({ viper, collection: name, syncMode: mode }));
301
- byName.set(key, found);
302
- }
303
- return found;
269
+ return made(viper, `${mode}:${name}`, () => createCollection(viperCollectionOptions({
270
+ viper,
271
+ collection: name,
272
+ syncMode: mode,
273
+ })));
304
274
  }
305
275
  /**
306
276
  * The rows of a Viper collection as a live list, like `useRecords` but read
@@ -0,0 +1,21 @@
1
+ import type { AppIdentity, AppMember, AppRole } from "@polyphron/viper-client";
2
+ /** The signed-in user's current app role and collection permission ceilings. */
3
+ export declare function useAppIdentity(): {
4
+ identity: AppIdentity | null;
5
+ error: Error | null;
6
+ loading: boolean;
7
+ refresh: () => Promise<void>;
8
+ };
9
+ /** Opt-in app-admin role/member operations; successful edits refresh all data. */
10
+ export declare function useAppManagement(): {
11
+ putRole: (name: string, grid: AppRole["grid"], options?: Pick<import("@polyphron/viper-client").RequestOptions, "signal">) => Promise<void>;
12
+ deleteRole: (name: string, options?: Pick<import("@polyphron/viper-client").RequestOptions, "signal">) => Promise<void>;
13
+ putMember: (user: string, role: string, options?: Pick<import("@polyphron/viper-client").RequestOptions, "signal">) => Promise<void>;
14
+ deleteMember: (user: string, options?: Pick<import("@polyphron/viper-client").RequestOptions, "signal">) => Promise<void>;
15
+ identity: AppIdentity | null;
16
+ roles: AppRole[] | null;
17
+ members: AppMember[] | null;
18
+ error: Error | null;
19
+ loading: boolean;
20
+ refresh: () => Promise<void>;
21
+ };
@@ -0,0 +1,54 @@
1
+ import { useCallback, useEffect } from "react";
2
+ import { useSession } from "./auth.js";
3
+ import { asError, useLoaded } from "./loaded.js";
4
+ import { useViper } from "./provider.js";
5
+ /** Loads identity data scoped to this exact client and session. */
6
+ function useIdentityData(manage) {
7
+ const viper = useViper();
8
+ const session = useSession();
9
+ const { data, error, loading, reload } = useLoaded(session, useCallback(async (signal) => {
10
+ if (!session)
11
+ return null;
12
+ const identity = await viper.identity.me({ signal });
13
+ if (!manage)
14
+ return { identity, roles: null, members: null, error: null };
15
+ // A 403 on roles or members mustn't drop the role me() just gave.
16
+ try {
17
+ const [roles, members] = await Promise.all([
18
+ viper.identity.roles({ signal }),
19
+ viper.identity.members({ signal }),
20
+ ]);
21
+ return { identity, roles, members, error: null };
22
+ }
23
+ catch (cause) {
24
+ return { identity, roles: null, members: null, error: asError(cause) };
25
+ }
26
+ }, [viper, session, manage]));
27
+ // Sign-in and role changes elsewhere in the app ask for a fresh load.
28
+ useEffect(() => viper.identity.onChange(reload), [viper, reload]);
29
+ return {
30
+ identity: data?.identity ?? null,
31
+ roles: data?.roles ?? null,
32
+ members: data?.members ?? null,
33
+ error: data?.error ?? (error ? asError(error) : null),
34
+ loading,
35
+ refresh: reload,
36
+ };
37
+ }
38
+ /** The signed-in user's current app role and collection permission ceilings. */
39
+ export function useAppIdentity() {
40
+ const { identity, error, loading, refresh } = useIdentityData(false);
41
+ return { identity, error, loading, refresh };
42
+ }
43
+ /** Opt-in app-admin role/member operations; successful edits refresh all data. */
44
+ export function useAppManagement() {
45
+ const viper = useViper();
46
+ const state = useIdentityData(true);
47
+ return {
48
+ ...state,
49
+ putRole: viper.identity.putRole,
50
+ deleteRole: viper.identity.deleteRole,
51
+ putMember: viper.identity.putMember,
52
+ deleteMember: viper.identity.deleteMember,
53
+ };
54
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { ViperProvider, useViper, type ViperProviderProps } from "./provider.js";
2
- export { useAuth, useAuthMethods, useSession, useInvites, useSessions, useTenants, useUser, } from "./auth.js";
2
+ export { useAuth, useAuthMethods, useSession, useSessionOf, useInvites, useSessions, useTenants, useUser, } from "./auth.js";
3
3
  export { useRealtime, useRealtimeStatus } from "./realtime.js";
4
- export { useRecord, useRecords, type Query, type RecordsOptions, } from "./records.js";
4
+ export { useLoaded, type Query } from "./loaded.js";
5
+ export { useRecord, useRecords, type RecordsOptions } from "./records.js";
5
6
  export { useRun, type RunState } from "./runs.js";
6
- export { Viper, ViperError, fieldError, memoryStorage, type ListOptions, type ListResult, type RealtimeEvent, type RealtimeStatus, type RecordInput, type RecordModel, type Session, type UserSession, type StartedRun, type ViperOptions, type WorkflowRun, } from "@polyphron/viper-client";
7
+ export { useAppIdentity, useAppManagement } from "./identity.js";
8
+ export { type AppIdentity, type AppMember, type AppRole, type AppLevel, Viper, ViperError, fieldError, memoryStorage, type ListOptions, type ListResult, type RealtimeEvent, type RealtimeStatus, type RecordInput, type RecordModel, type Session, type UserSession, type StartedRun, type ViperOptions, type WorkflowRun, } from "@polyphron/viper-client";
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  export { ViperProvider, useViper } from "./provider.js";
2
- export { useAuth, useAuthMethods, useSession, useInvites, useSessions, useTenants, useUser, } from "./auth.js";
2
+ export { useAuth, useAuthMethods, useSession, useSessionOf, useInvites, useSessions, useTenants, useUser, } from "./auth.js";
3
3
  export { useRealtime, useRealtimeStatus } from "./realtime.js";
4
- export { useRecord, useRecords, } from "./records.js";
4
+ export { useLoaded } from "./loaded.js";
5
+ export { useRecord, useRecords } from "./records.js";
5
6
  export { useRun } from "./runs.js";
7
+ export { useAppIdentity, useAppManagement } from "./identity.js";
6
8
  // Re-exported so an app only needs @polyphron/viper-react in its imports.
7
9
  export { Viper, ViperError, fieldError, memoryStorage, } from "@polyphron/viper-client";
@@ -0,0 +1,30 @@
1
+ /** What a loading hook hands back. */
2
+ export type Query<T> = {
3
+ data: T | null;
4
+ error: unknown;
5
+ /**
6
+ * True until the first answer arrives for what is being shown (a new
7
+ * collection, a new record id, another user). A `reload` or a live update
8
+ * does not set it again, so the last data stays on screen; see
9
+ * `refreshing`.
10
+ */
11
+ loading: boolean;
12
+ /** True whenever a request is in flight, first load and reloads included. */
13
+ refreshing: boolean;
14
+ /** Fetches again now. The promise settles once the new answer is in. */
15
+ reload: () => Promise<void>;
16
+ };
17
+ /** Makes an `Error` out of whatever a failed load threw. */
18
+ export declare function asError(cause: unknown): Error;
19
+ /**
20
+ * The one fetch hook: runs `run` whenever it or `subject` changes, and gives
21
+ * back its state. Answers from a call that has already been replaced are
22
+ * dropped, so a slow first request can't overwrite a fast second one.
23
+ *
24
+ * `subject` names what is being shown (a collection, a user, a session);
25
+ * data from another subject is never handed back, only data from a reload of
26
+ * the same one. Subjects are compared with `Object.is`. `reload` fetches in
27
+ * its own call, not through a render, so awaiting it inside React's `act`
28
+ * (or any batched update) can't deadlock.
29
+ */
30
+ export declare function useLoaded<T>(subject: unknown, run: (signal: AbortSignal) => Promise<T | null>): Query<T>;