@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/README.md +26 -0
- package/dist/auth.d.ts +25 -4
- package/dist/auth.js +59 -136
- package/dist/db-fetch.js +61 -89
- package/dist/db-page.js +38 -74
- package/dist/db-query.d.ts +0 -6
- package/dist/db-query.js +8 -8
- package/dist/db-shared.d.ts +50 -0
- package/dist/db-shared.js +82 -0
- package/dist/db.js +38 -68
- package/dist/identity.d.ts +21 -0
- package/dist/identity.js +54 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/loaded.d.ts +30 -0
- package/dist/loaded.js +64 -0
- package/dist/page-plan.js +1 -4
- package/dist/records.d.ts +1 -14
- package/dist/records.js +4 -39
- package/dist/runs.d.ts +2 -2
- package/dist/runs.js +7 -6
- package/package.json +2 -2
package/dist/loaded.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
/** Makes an `Error` out of whatever a failed load threw. */
|
|
3
|
+
export function asError(cause) {
|
|
4
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The one fetch hook: runs `run` whenever it or `subject` changes, and gives
|
|
8
|
+
* back its state. Answers from a call that has already been replaced are
|
|
9
|
+
* dropped, so a slow first request can't overwrite a fast second one.
|
|
10
|
+
*
|
|
11
|
+
* `subject` names what is being shown (a collection, a user, a session);
|
|
12
|
+
* data from another subject is never handed back, only data from a reload of
|
|
13
|
+
* the same one. Subjects are compared with `Object.is`. `reload` fetches in
|
|
14
|
+
* its own call, not through a render, so awaiting it inside React's `act`
|
|
15
|
+
* (or any batched update) can't deadlock.
|
|
16
|
+
*/
|
|
17
|
+
export function useLoaded(subject, run) {
|
|
18
|
+
const [inFlight, setInFlight] = useState(0);
|
|
19
|
+
const [answer, setAnswer] = useState({ subject: null, run: null, data: null, error: null });
|
|
20
|
+
// The newest request's controller and promise: starting one replaces the
|
|
21
|
+
// last, and a replaced request waits for the one that replaced it.
|
|
22
|
+
const controller = useRef(null);
|
|
23
|
+
const newest = useRef(Promise.resolve());
|
|
24
|
+
const fetchNow = useCallback((forSubject, forRun) => {
|
|
25
|
+
controller.current?.abort();
|
|
26
|
+
const mine = new AbortController();
|
|
27
|
+
controller.current = mine;
|
|
28
|
+
const request = (async () => {
|
|
29
|
+
// After a tick, so starting from an effect sets no state in it.
|
|
30
|
+
await Promise.resolve();
|
|
31
|
+
setInFlight((n) => n + 1);
|
|
32
|
+
try {
|
|
33
|
+
const data = await forRun(mine.signal);
|
|
34
|
+
if (!mine.signal.aborted) {
|
|
35
|
+
setAnswer({ subject: forSubject, run: forRun, data, error: null });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (!mine.signal.aborted) {
|
|
40
|
+
setAnswer({ subject: forSubject, run: forRun, data: null, error });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
setInFlight((n) => n - 1);
|
|
45
|
+
}
|
|
46
|
+
})();
|
|
47
|
+
newest.current = request;
|
|
48
|
+
return request.then(() => mine.signal.aborted ? newest.current : undefined);
|
|
49
|
+
}, []);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
void fetchNow(subject, run);
|
|
52
|
+
return () => controller.current?.abort();
|
|
53
|
+
}, [subject, run, fetchNow]);
|
|
54
|
+
const sameSubject = answer.run !== null && Object.is(answer.subject, subject);
|
|
55
|
+
const current = answer.run === run && inFlight === 0;
|
|
56
|
+
return {
|
|
57
|
+
// The last good data stays on screen while a reload is in flight.
|
|
58
|
+
data: sameSubject ? answer.data : null,
|
|
59
|
+
error: current ? answer.error : null,
|
|
60
|
+
loading: !sameSubject,
|
|
61
|
+
refreshing: !current,
|
|
62
|
+
reload: useCallback(() => fetchNow(subject, run), [fetchNow, subject, run]),
|
|
63
|
+
};
|
|
64
|
+
}
|
package/dist/page-plan.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { same } from "./db-shared.js";
|
|
1
2
|
// Sort fields the browser orders just as Postgres does: times compared as
|
|
2
3
|
// times, and ids (a-z and 0-9) compared as text.
|
|
3
4
|
const TIMES = new Set(["created", "updated"]);
|
|
@@ -50,10 +51,6 @@ export function pageComparator(sort) {
|
|
|
50
51
|
return 0;
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
|
-
// True when two field values are the same, lists and objects included.
|
|
54
|
-
function same(a, b) {
|
|
55
|
-
return a === b || JSON.stringify(a) === JSON.stringify(b);
|
|
56
|
-
}
|
|
57
54
|
/** What `event` does to `page`. */
|
|
58
55
|
export function planPage(event, page) {
|
|
59
56
|
// A new row, or a view that changed: only the server knows where it goes.
|
package/dist/records.d.ts
CHANGED
|
@@ -1,18 +1,5 @@
|
|
|
1
1
|
import type { ListOptions, ListResult, RecordModel } from "@polyphron/viper-client";
|
|
2
|
-
|
|
3
|
-
data: T | null;
|
|
4
|
-
error: unknown;
|
|
5
|
-
/**
|
|
6
|
-
* True until the first answer arrives for what is being shown (a new
|
|
7
|
-
* collection, or a new record id). A `reload` or a live update does not set
|
|
8
|
-
* it again, so the last data stays on screen; see `refreshing`.
|
|
9
|
-
*/
|
|
10
|
-
loading: boolean;
|
|
11
|
-
/** True whenever a request is in flight, first load and reloads included. */
|
|
12
|
-
refreshing: boolean;
|
|
13
|
-
/** Fetches again now. */
|
|
14
|
-
reload: () => void;
|
|
15
|
-
};
|
|
2
|
+
import { type Query } from "./loaded.js";
|
|
16
3
|
export type RecordsOptions = ListOptions & {
|
|
17
4
|
/** Re-fetch when anything in the collection changes. Off by default. */
|
|
18
5
|
live?: boolean;
|
package/dist/records.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useCallback, useEffect
|
|
1
|
+
import { useCallback, useEffect } from "react";
|
|
2
|
+
import { useLoaded } from "./loaded.js";
|
|
2
3
|
import { useViper } from "./provider.js";
|
|
3
4
|
import { useRealtime } from "./realtime.js";
|
|
4
5
|
/**
|
|
@@ -13,7 +14,7 @@ export function useRecords(collection, options = {}) {
|
|
|
13
14
|
const { live = false, ...list } = options;
|
|
14
15
|
// Options are a fresh object every render; compare them by value.
|
|
15
16
|
const key = JSON.stringify(list);
|
|
16
|
-
const query =
|
|
17
|
+
const query = useLoaded(collection, useCallback((signal) => viper
|
|
17
18
|
.collection(collection)
|
|
18
19
|
.list({ ...JSON.parse(key), signal }), [viper, collection, key]));
|
|
19
20
|
useLiveReload(live, collection, query.reload, viper);
|
|
@@ -23,7 +24,7 @@ export function useRecords(collection, options = {}) {
|
|
|
23
24
|
export function useRecord(collection, id, options = {}) {
|
|
24
25
|
const viper = useViper();
|
|
25
26
|
const { live = false, expand } = options;
|
|
26
|
-
const query =
|
|
27
|
+
const query = useLoaded(
|
|
27
28
|
// A different record must never show the last one's data.
|
|
28
29
|
`${collection}/${id ?? ""}`, useCallback((signal) => id
|
|
29
30
|
? viper.collection(collection).view(id, { expand, signal })
|
|
@@ -31,42 +32,6 @@ export function useRecord(collection, id, options = {}) {
|
|
|
31
32
|
useLiveReload(live && Boolean(id), `${collection}/${id}`, query.reload, viper);
|
|
32
33
|
return query;
|
|
33
34
|
}
|
|
34
|
-
// Runs `run` whenever it changes, and gives back its state. Answers from a
|
|
35
|
-
// call that has already been replaced are dropped, so a slow first request
|
|
36
|
-
// can't overwrite a fast second one.
|
|
37
|
-
//
|
|
38
|
-
// `refreshing` and `loading` are worked out rather than stored: the answer
|
|
39
|
-
// carries the request it belongs to, so anything older than the current
|
|
40
|
-
// request means "still waiting" — and no state has to be set while the effect
|
|
41
|
-
// is starting. `subject` names what is being shown; data from another subject
|
|
42
|
-
// is never handed back, only data from a reload of the same one.
|
|
43
|
-
function useFetch(subject, run) {
|
|
44
|
-
const [attempt, setAttempt] = useState(0);
|
|
45
|
-
const [answer, setAnswer] = useState({ subject: "", run: null, attempt: -1, data: null, error: null });
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
const controller = new AbortController();
|
|
48
|
-
run(controller.signal).then((data) => {
|
|
49
|
-
if (!controller.signal.aborted) {
|
|
50
|
-
setAnswer({ subject, run, attempt, data, error: null });
|
|
51
|
-
}
|
|
52
|
-
}, (error) => {
|
|
53
|
-
if (!controller.signal.aborted) {
|
|
54
|
-
setAnswer({ subject, run, attempt, data: null, error });
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
return () => controller.abort();
|
|
58
|
-
}, [subject, run, attempt]);
|
|
59
|
-
const current = answer.run === run && answer.attempt === attempt;
|
|
60
|
-
const sameSubject = answer.run !== null && answer.subject === subject;
|
|
61
|
-
return {
|
|
62
|
-
// The last good data stays on screen while a reload is in flight.
|
|
63
|
-
data: sameSubject ? answer.data : null,
|
|
64
|
-
error: current ? answer.error : null,
|
|
65
|
-
loading: !sameSubject,
|
|
66
|
-
refreshing: !current,
|
|
67
|
-
reload: useCallback(() => setAttempt((n) => n + 1), []),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
35
|
// Reloads on every realtime event for `topic`, when `live` is on, and once
|
|
71
36
|
// more after the socket comes back from a drop, since events may have been
|
|
72
37
|
// missed while it was down. The hooks are always called, because hooks can't
|
package/dist/runs.d.ts
CHANGED
|
@@ -3,11 +3,11 @@ export type RunState<T> = {
|
|
|
3
3
|
/** The run as last read, or null before the first answer. */
|
|
4
4
|
data: WorkflowRun<T> | null;
|
|
5
5
|
error: unknown;
|
|
6
|
-
/** True from the start until the run
|
|
6
|
+
/** True from the start until the run has ended, or can't be read. */
|
|
7
7
|
pending: boolean;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
* Watches a background workflow's run until it's done or
|
|
10
|
+
* Watches a background workflow's run until it's done, failed or cancelled, reading
|
|
11
11
|
* it every `every` milliseconds (a second by default). Pass the
|
|
12
12
|
* `StartedRun` its call returned; nothing is read while it's empty.
|
|
13
13
|
*
|
package/dist/runs.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { useViper } from "./provider.js";
|
|
3
|
+
/** A run is still going while it runs or waits; done, failed and cancelled are final. */
|
|
4
|
+
function unfinished(run) {
|
|
5
|
+
return run.status === "running" || run.status === "waiting";
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
|
-
* Watches a background workflow's run until it's done or
|
|
8
|
+
* Watches a background workflow's run until it's done, failed or cancelled, reading
|
|
5
9
|
* it every `every` milliseconds (a second by default). Pass the
|
|
6
10
|
* `StartedRun` its call returned; nothing is read while it's empty.
|
|
7
11
|
*
|
|
@@ -29,7 +33,7 @@ export function useRun(name, started, options = {}) {
|
|
|
29
33
|
try {
|
|
30
34
|
const data = await viper.workflows.run(name, { run, token }, { signal: controller.signal });
|
|
31
35
|
setState({ key, data, error: null });
|
|
32
|
-
if (data
|
|
36
|
+
if (unfinished(data)) {
|
|
33
37
|
timer = setTimeout(() => void read(), every);
|
|
34
38
|
}
|
|
35
39
|
}
|
|
@@ -50,9 +54,6 @@ export function useRun(name, started, options = {}) {
|
|
|
50
54
|
return {
|
|
51
55
|
data,
|
|
52
56
|
error,
|
|
53
|
-
pending: run !== undefined &&
|
|
54
|
-
!error &&
|
|
55
|
-
data?.status !== "done" &&
|
|
56
|
-
data?.status !== "failed",
|
|
57
|
+
pending: run !== undefined && !error && (data === null || unfinished(data)),
|
|
57
58
|
};
|
|
58
59
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polyphron/viper-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "React provider and hooks for the Viper app API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"typecheck": "tsc --noEmit"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@polyphron/viper-client": "^0.
|
|
33
|
+
"@polyphron/viper-client": "^0.4.0",
|
|
34
34
|
"@tanstack/db": "0.9.2",
|
|
35
35
|
"@tanstack/react-db": "0.4.1",
|
|
36
36
|
"react": "^18.3.0 || ^19.0.0"
|