@polyphron/viper-react 0.3.0 → 0.5.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 +25 -0
- package/dist/auth.d.ts +28 -7
- package/dist/auth.js +62 -139
- 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 +6 -4
- package/dist/index.js +5 -3
- package/dist/loaded.d.ts +30 -0
- package/dist/loaded.js +64 -0
- package/dist/page-plan.js +1 -4
- package/dist/realtime.d.ts +12 -1
- package/dist/realtime.js +50 -1
- package/dist/records.d.ts +16 -15
- package/dist/records.js +26 -42
- package/dist/runs.d.ts +2 -2
- package/dist/runs.js +7 -6
- package/dist/upload.d.ts +24 -0
- package/dist/upload.js +44 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -71,6 +71,17 @@ function SignIn() {
|
|
|
71
71
|
|
|
72
72
|
## The hooks
|
|
73
73
|
|
|
74
|
+
With personal tokens or device sign-in enabled, `useAuth().auth` exposes
|
|
75
|
+
`createToken`, `tokens`, `revokeToken`, `startDeviceSignIn`, `authorizeDevice`
|
|
76
|
+
and `pollDeviceSignIn`. Polling an approved device saves its normal session,
|
|
77
|
+
so `useUser()` updates. Only display the user code and verification URI;
|
|
78
|
+
keep the device code private and honor each polling result's interval.
|
|
79
|
+
|
|
80
|
+
With email change confirmation enabled, `useAuth()` exposes
|
|
81
|
+
`auth.requestEmailChange(newEmail)` and `auth.confirmEmailChange(token)`.
|
|
82
|
+
Both need the current user's session. After confirming, call `auth.me()`
|
|
83
|
+
to refresh the email shown by `useUser()`.
|
|
84
|
+
|
|
74
85
|
| Hook | What it gives you |
|
|
75
86
|
| --- | --- |
|
|
76
87
|
| `useViper()` | The `Viper` client — writes go through it: `viper.collection("posts").create(…)` |
|
|
@@ -78,6 +89,12 @@ function SignIn() {
|
|
|
78
89
|
| `useRecord(collection, id, options)` | One record, same shape |
|
|
79
90
|
| `useAuth(collection)` | `{ user, auth }` for an auth collection |
|
|
80
91
|
| `useSession()` / `useUser()` | The session / the logged-in user, re-rendering on change |
|
|
92
|
+
| `useSessionOf(client, read)` | The same for any client with `onSessionChange` (the admin client, say); `read` returns what to show, like `() => admin.user` |
|
|
93
|
+
| `useAuthMethods({ collection?, enabled? })` | How to sign in here (methods, sign-up, project branding), or `null` while it loads |
|
|
94
|
+
| `useSessions()` | The user's active sessions: `{ sessions, error, loading, refresh, revoke, logoutEverywhere }` |
|
|
95
|
+
| `useInvites()` | Invites the user sent: `{ invites, error, loading, refresh, invite, cancel }` |
|
|
96
|
+
| `useTenants(name)` | The user's memberships of tenant collection `name`; never the last user's list |
|
|
97
|
+
| `useRun(name, started, { every? })` | Follows a background workflow's run until it ends: `{ data, error, pending }` |
|
|
81
98
|
| `useRealtime(topics, onEvent)` | Watch changes yourself; returns the connection status |
|
|
82
99
|
| `useRealtimeStatus()` | `"closed"`, `"connecting"` or `"live"` |
|
|
83
100
|
|
|
@@ -95,6 +112,14 @@ flight, so lists don't flash.
|
|
|
95
112
|
- With `live: true`, the list is also re-fetched once after the socket comes
|
|
96
113
|
back from a drop, since events may have been missed while it was down.
|
|
97
114
|
|
|
115
|
+
`useSessions` and `useInvites` both give `loading` (true
|
|
116
|
+
until the first answer for this user), `error` (an `Error` or `null`) and
|
|
117
|
+
`refresh()`, which resolves once the new answer is in. They key their state on
|
|
118
|
+
the signed-in user, so after a user switch you never see the last user's data.
|
|
119
|
+
`useLoaded(subject, load)` is the helper they and `useRecords` are built on;
|
|
120
|
+
use it to write your own fetching hook. It returns the same `Query` shape as
|
|
121
|
+
`useRecords`.
|
|
122
|
+
|
|
98
123
|
`@polyphron/viper-client` is a peer dependency: install it next to this
|
|
99
124
|
package, so the app and the hooks share one client.
|
|
100
125
|
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import type { AuthMethods, AuthService, Invite, InviteInput, RecordModel, Session, UserSession } from "@polyphron/viper-client";
|
|
2
|
+
/**
|
|
3
|
+
* The session of any client that announces changes (`Viper`, `ViperAdmin`),
|
|
4
|
+
* re-rendering when it changes. `read` returns what to show from the client,
|
|
5
|
+
* like `() => admin.user`. It must be stable between changes.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useSessionOf<S>(client: {
|
|
8
|
+
onSessionChange(listener: () => void): () => void;
|
|
9
|
+
}, read: () => S | null): S | null;
|
|
2
10
|
/** The current session, re-rendering on login, logout and refresh. */
|
|
3
11
|
export declare function useSession(): Session | null;
|
|
4
12
|
/** The signed-in user's active sessions, with revoke and log-out actions. */
|
|
5
13
|
export declare function useSessions(): {
|
|
6
14
|
sessions: UserSession[] | null;
|
|
7
15
|
error: Error | null;
|
|
16
|
+
loading: boolean;
|
|
8
17
|
refresh: () => Promise<void>;
|
|
9
18
|
revoke: (sessionId: string) => Promise<void>;
|
|
10
19
|
logoutEverywhere: () => Promise<void>;
|
|
@@ -13,6 +22,7 @@ export declare function useSessions(): {
|
|
|
13
22
|
export declare function useInvites(): {
|
|
14
23
|
invites: Invite[] | null;
|
|
15
24
|
error: Error | null;
|
|
25
|
+
loading: boolean;
|
|
16
26
|
refresh: () => Promise<void>;
|
|
17
27
|
invite: (input: InviteInput) => Promise<Invite>;
|
|
18
28
|
cancel: (id: string) => Promise<void>;
|
|
@@ -27,6 +37,12 @@ export declare function useUser<T extends RecordModel = RecordModel>(): T | null
|
|
|
27
37
|
* ```tsx
|
|
28
38
|
* const { user, auth } = useAuth()
|
|
29
39
|
* await auth.loginWithPassword(email, password)
|
|
40
|
+
* await auth.requestEmailChange(newEmail)
|
|
41
|
+
* await auth.confirmEmailChange(token)
|
|
42
|
+
* await auth.me() // refresh the user after confirmation
|
|
43
|
+
* const proof = await auth.beginOAuth2("google", location.href)
|
|
44
|
+
* // Keep proof.codeVerifier across navigation to proof.url.
|
|
45
|
+
* const links = await auth.oauth2Links()
|
|
30
46
|
* ```
|
|
31
47
|
*/
|
|
32
48
|
export declare function useAuth<T extends RecordModel = RecordModel>(collection?: string): {
|
|
@@ -34,21 +50,26 @@ export declare function useAuth<T extends RecordModel = RecordModel>(collection?
|
|
|
34
50
|
auth: AuthService<T>;
|
|
35
51
|
};
|
|
36
52
|
/**
|
|
37
|
-
* How to sign in here: the ways on offer, sign-up, and the
|
|
38
|
-
*
|
|
39
|
-
* it loads
|
|
53
|
+
* How to sign in here: the ways on offer, sign-up, and the project's
|
|
54
|
+
* branding. Null while
|
|
55
|
+
* it loads, and when the load fails: the sign-in calls report their own
|
|
56
|
+
* errors. Name an auth `collection` when the project has several, and pass
|
|
57
|
+
* `enabled: false` to hold the request back. For a sign-in page.
|
|
40
58
|
*
|
|
41
59
|
* ```tsx
|
|
42
60
|
* const methods = useAuthMethods()
|
|
43
|
-
* return <h1>{methods?.
|
|
61
|
+
* return <h1>{methods?.branding?.name}</h1>
|
|
44
62
|
* ```
|
|
45
63
|
*/
|
|
46
|
-
export declare function useAuthMethods(
|
|
64
|
+
export declare function useAuthMethods(options?: {
|
|
65
|
+
collection?: string;
|
|
66
|
+
enabled?: boolean;
|
|
67
|
+
}): AuthMethods | null;
|
|
47
68
|
/**
|
|
48
69
|
* The signed-in user's memberships of tenant collection `name` (like
|
|
49
70
|
* "orgs"), each with its role and the tenant under `expand.tenant`. Empty
|
|
50
|
-
* while signed out, null while it loads
|
|
51
|
-
* changes.
|
|
71
|
+
* while signed out, null while it loads (and when the load fails). It
|
|
72
|
+
* loads again when the user changes, and never shows the last user's list.
|
|
52
73
|
*
|
|
53
74
|
* ```tsx
|
|
54
75
|
* const orgs = useTenants("orgs")
|
package/dist/auth.js
CHANGED
|
@@ -1,127 +1,55 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useSyncExternalStore } from "react";
|
|
2
|
+
import { asError, useLoaded } from "./loaded.js";
|
|
2
3
|
import { useViper } from "./provider.js";
|
|
4
|
+
/**
|
|
5
|
+
* The session of any client that announces changes (`Viper`, `ViperAdmin`),
|
|
6
|
+
* re-rendering when it changes. `read` returns what to show from the client,
|
|
7
|
+
* like `() => admin.user`. It must be stable between changes.
|
|
8
|
+
*/
|
|
9
|
+
export function useSessionOf(client, read) {
|
|
10
|
+
return useSyncExternalStore((listener) => client.onSessionChange(listener), read, () => null);
|
|
11
|
+
}
|
|
3
12
|
/** The current session, re-rendering on login, logout and refresh. */
|
|
4
13
|
export function useSession() {
|
|
5
14
|
const viper = useViper();
|
|
6
|
-
return
|
|
15
|
+
return useSessionOf(viper, () => viper.session);
|
|
7
16
|
}
|
|
8
17
|
/** The signed-in user's active sessions, with revoke and log-out actions. */
|
|
9
18
|
export function useSessions() {
|
|
10
19
|
const viper = useViper();
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const refresh = useCallback(async () => {
|
|
14
|
-
const userId = user?.id;
|
|
15
|
-
if (!userId)
|
|
16
|
-
return;
|
|
17
|
-
try {
|
|
18
|
-
setLoaded({
|
|
19
|
-
userId,
|
|
20
|
-
sessions: await viper.auth.sessions(),
|
|
21
|
-
error: null,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
catch (cause) {
|
|
25
|
-
setLoaded({
|
|
26
|
-
userId,
|
|
27
|
-
sessions: [],
|
|
28
|
-
error: cause instanceof Error ? cause : new Error(String(cause)),
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
}, [user?.id, viper]);
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
const ctl = new AbortController();
|
|
34
|
-
const userId = user?.id;
|
|
35
|
-
if (userId) {
|
|
36
|
-
viper.auth
|
|
37
|
-
.sessions({ signal: ctl.signal })
|
|
38
|
-
.then((sessions) => setLoaded({ userId, sessions, error: null }))
|
|
39
|
-
.catch((cause) => {
|
|
40
|
-
if (!ctl.signal.aborted) {
|
|
41
|
-
setLoaded({
|
|
42
|
-
userId,
|
|
43
|
-
sessions: [],
|
|
44
|
-
error: cause instanceof Error ? cause : new Error(String(cause)),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
return () => ctl.abort();
|
|
50
|
-
}, [viper, user?.id]);
|
|
51
|
-
const revoke = async (sessionId) => {
|
|
52
|
-
await viper.auth.revokeSession(sessionId);
|
|
53
|
-
await refresh();
|
|
54
|
-
};
|
|
55
|
-
const logoutEverywhere = () => viper.auth.logoutEverywhere();
|
|
56
|
-
const current = loaded?.userId === user?.id ? loaded : null;
|
|
20
|
+
const userId = useUser()?.id ?? null;
|
|
21
|
+
const { data, error, loading, reload } = useLoaded(userId, useCallback((signal) => userId ? viper.auth.sessions({ signal }) : Promise.resolve(null), [viper, userId]));
|
|
57
22
|
return {
|
|
58
|
-
sessions:
|
|
59
|
-
error:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
23
|
+
sessions: data,
|
|
24
|
+
error: error ? asError(error) : null,
|
|
25
|
+
loading,
|
|
26
|
+
refresh: reload,
|
|
27
|
+
revoke: async (sessionId) => {
|
|
28
|
+
await viper.auth.revokeSession(sessionId);
|
|
29
|
+
await reload();
|
|
30
|
+
},
|
|
31
|
+
logoutEverywhere: () => viper.auth.logoutEverywhere(),
|
|
63
32
|
};
|
|
64
33
|
}
|
|
65
34
|
/** The invites the signed-in user sent, with send and take-back actions. */
|
|
66
35
|
export function useInvites() {
|
|
67
36
|
const viper = useViper();
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const refresh = useCallback(async () => {
|
|
71
|
-
const userId = user?.id;
|
|
72
|
-
if (!userId)
|
|
73
|
-
return;
|
|
74
|
-
try {
|
|
75
|
-
setLoaded({ userId, invites: await viper.auth.invites(), error: null });
|
|
76
|
-
}
|
|
77
|
-
catch (cause) {
|
|
78
|
-
setLoaded({
|
|
79
|
-
userId,
|
|
80
|
-
invites: [],
|
|
81
|
-
error: cause instanceof Error ? cause : new Error(String(cause)),
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}, [user?.id, viper]);
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
let live = true;
|
|
87
|
-
const userId = user?.id;
|
|
88
|
-
if (userId) {
|
|
89
|
-
viper.auth
|
|
90
|
-
.invites()
|
|
91
|
-
.then((invites) => {
|
|
92
|
-
if (live)
|
|
93
|
-
setLoaded({ userId, invites, error: null });
|
|
94
|
-
})
|
|
95
|
-
.catch((cause) => {
|
|
96
|
-
if (live) {
|
|
97
|
-
setLoaded({
|
|
98
|
-
userId,
|
|
99
|
-
invites: [],
|
|
100
|
-
error: cause instanceof Error ? cause : new Error(String(cause)),
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return () => {
|
|
106
|
-
live = false;
|
|
107
|
-
};
|
|
108
|
-
}, [viper, user?.id]);
|
|
109
|
-
const invite = async (input) => {
|
|
110
|
-
const made = await viper.auth.invite(input);
|
|
111
|
-
await refresh();
|
|
112
|
-
return made;
|
|
113
|
-
};
|
|
114
|
-
const cancel = async (id) => {
|
|
115
|
-
await viper.auth.cancelInvite(id);
|
|
116
|
-
await refresh();
|
|
117
|
-
};
|
|
118
|
-
const current = loaded?.userId === user?.id ? loaded : null;
|
|
37
|
+
const userId = useUser()?.id ?? null;
|
|
38
|
+
const { data, error, loading, reload } = useLoaded(userId, useCallback((signal) => userId ? viper.auth.invites({ signal }) : Promise.resolve(null), [viper, userId]));
|
|
119
39
|
return {
|
|
120
|
-
invites:
|
|
121
|
-
error:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
40
|
+
invites: data,
|
|
41
|
+
error: error ? asError(error) : null,
|
|
42
|
+
loading,
|
|
43
|
+
refresh: reload,
|
|
44
|
+
invite: async (input) => {
|
|
45
|
+
const made = await viper.auth.invite(input);
|
|
46
|
+
await reload();
|
|
47
|
+
return made;
|
|
48
|
+
},
|
|
49
|
+
cancel: async (id) => {
|
|
50
|
+
await viper.auth.cancelInvite(id);
|
|
51
|
+
await reload();
|
|
52
|
+
},
|
|
125
53
|
};
|
|
126
54
|
}
|
|
127
55
|
/** The logged-in user, or null when signed out. */
|
|
@@ -136,6 +64,12 @@ export function useUser() {
|
|
|
136
64
|
* ```tsx
|
|
137
65
|
* const { user, auth } = useAuth()
|
|
138
66
|
* await auth.loginWithPassword(email, password)
|
|
67
|
+
* await auth.requestEmailChange(newEmail)
|
|
68
|
+
* await auth.confirmEmailChange(token)
|
|
69
|
+
* await auth.me() // refresh the user after confirmation
|
|
70
|
+
* const proof = await auth.beginOAuth2("google", location.href)
|
|
71
|
+
* // Keep proof.codeVerifier across navigation to proof.url.
|
|
72
|
+
* const links = await auth.oauth2Links()
|
|
139
73
|
* ```
|
|
140
74
|
*/
|
|
141
75
|
export function useAuth(collection) {
|
|
@@ -147,34 +81,32 @@ export function useAuth(collection) {
|
|
|
147
81
|
return { user, auth };
|
|
148
82
|
}
|
|
149
83
|
/**
|
|
150
|
-
* How to sign in here: the ways on offer, sign-up, and the
|
|
151
|
-
*
|
|
152
|
-
* it loads
|
|
84
|
+
* How to sign in here: the ways on offer, sign-up, and the project's
|
|
85
|
+
* branding. Null while
|
|
86
|
+
* it loads, and when the load fails: the sign-in calls report their own
|
|
87
|
+
* errors. Name an auth `collection` when the project has several, and pass
|
|
88
|
+
* `enabled: false` to hold the request back. For a sign-in page.
|
|
153
89
|
*
|
|
154
90
|
* ```tsx
|
|
155
91
|
* const methods = useAuthMethods()
|
|
156
|
-
* return <h1>{methods?.
|
|
92
|
+
* return <h1>{methods?.branding?.name}</h1>
|
|
157
93
|
* ```
|
|
158
94
|
*/
|
|
159
|
-
export function useAuthMethods() {
|
|
95
|
+
export function useAuthMethods(options = {}) {
|
|
160
96
|
const viper = useViper();
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.methods({ signal
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.catch(() => { });
|
|
169
|
-
return () => ctl.abort();
|
|
170
|
-
}, [viper]);
|
|
171
|
-
return methods;
|
|
97
|
+
const { collection, enabled = true } = options;
|
|
98
|
+
const { data } = useLoaded(`${collection ?? ""}/${enabled}`, useCallback((signal) => enabled
|
|
99
|
+
? (collection
|
|
100
|
+
? viper.collection(collection).auth
|
|
101
|
+
: viper.auth).methods({ signal })
|
|
102
|
+
: Promise.resolve(null), [viper, collection, enabled]));
|
|
103
|
+
return data;
|
|
172
104
|
}
|
|
173
105
|
/**
|
|
174
106
|
* The signed-in user's memberships of tenant collection `name` (like
|
|
175
107
|
* "orgs"), each with its role and the tenant under `expand.tenant`. Empty
|
|
176
|
-
* while signed out, null while it loads
|
|
177
|
-
* changes.
|
|
108
|
+
* while signed out, null while it loads (and when the load fails). It
|
|
109
|
+
* loads again when the user changes, and never shows the last user's list.
|
|
178
110
|
*
|
|
179
111
|
* ```tsx
|
|
180
112
|
* const orgs = useTenants("orgs")
|
|
@@ -183,16 +115,7 @@ export function useAuthMethods() {
|
|
|
183
115
|
*/
|
|
184
116
|
export function useTenants(name) {
|
|
185
117
|
const viper = useViper();
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
const ctl = new AbortController();
|
|
190
|
-
viper
|
|
191
|
-
.tenants(name, { signal: ctl.signal })
|
|
192
|
-
.then(setTenants)
|
|
193
|
-
// A failed load stays null; record calls report their own errors.
|
|
194
|
-
.catch(() => { });
|
|
195
|
-
return () => ctl.abort();
|
|
196
|
-
}, [viper, name, user?.id]);
|
|
197
|
-
return tenants;
|
|
118
|
+
const userId = useUser()?.id ?? "";
|
|
119
|
+
const { data } = useLoaded(`${name}/${userId}`, useCallback((signal) => viper.tenants(name, { signal }), [viper, name]));
|
|
120
|
+
return data;
|
|
198
121
|
}
|
package/dist/db-fetch.js
CHANGED
|
@@ -9,6 +9,7 @@ import { ViperError } from "@polyphron/viper-client";
|
|
|
9
9
|
import { createCollection } from "@tanstack/db";
|
|
10
10
|
import { useLiveQuery } from "@tanstack/react-db";
|
|
11
11
|
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
12
|
+
import { cacheOf, same, singleFlight } from "./db-shared.js";
|
|
12
13
|
// The key a one-object fetch keeps its row under.
|
|
13
14
|
const ONE = "one";
|
|
14
15
|
// Loads that failed on the network (not "no" or "not found") try again.
|
|
@@ -18,10 +19,6 @@ const watched = new Set();
|
|
|
18
19
|
if (typeof window !== "undefined") {
|
|
19
20
|
window.addEventListener("focus", () => watched.forEach((entry) => entry.onFocus()));
|
|
20
21
|
}
|
|
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
22
|
// Waits `ms`, or less if `signal` aborts.
|
|
26
23
|
function wait(ms, signal) {
|
|
27
24
|
return new Promise((done) => {
|
|
@@ -66,8 +63,6 @@ export function fetchCollectionOptions(options) {
|
|
|
66
63
|
const { begin, write, commit, markReady } = params;
|
|
67
64
|
const rows = new Map();
|
|
68
65
|
let closed = false;
|
|
69
|
-
let loading = null;
|
|
70
|
-
let again = false;
|
|
71
66
|
let timer;
|
|
72
67
|
const controller = new AbortController();
|
|
73
68
|
// Writes only what changed, so rows that stayed the same keep their
|
|
@@ -118,59 +113,48 @@ export function fetchCollectionOptions(options) {
|
|
|
118
113
|
: void load(), every);
|
|
119
114
|
}
|
|
120
115
|
};
|
|
121
|
-
const load =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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();
|
|
116
|
+
const load = singleFlight({
|
|
117
|
+
start: () => publish({ fetching: true }),
|
|
118
|
+
pass: async () => {
|
|
119
|
+
let data;
|
|
120
|
+
const at = generation;
|
|
121
|
+
for (let tries = 0;; tries++) {
|
|
122
|
+
try {
|
|
123
|
+
data = await fetch(controller.signal);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
// The server said no. No answer at all (status 0) and
|
|
128
|
+
// server trouble are worth another go.
|
|
129
|
+
const refused = error instanceof ViperError &&
|
|
130
|
+
error.status > 0 &&
|
|
131
|
+
error.status < 500;
|
|
132
|
+
if (closed || refused || tries >= RETRIES)
|
|
133
|
+
throw error;
|
|
134
|
+
await wait(1000 * 2 ** tries, controller.signal);
|
|
135
|
+
}
|
|
170
136
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
137
|
+
if (closed)
|
|
138
|
+
return;
|
|
139
|
+
// set() put newer data in while this was on the way.
|
|
140
|
+
if (at === generation)
|
|
141
|
+
apply(data);
|
|
142
|
+
},
|
|
143
|
+
closed: () => closed,
|
|
144
|
+
fail: (error) => {
|
|
145
|
+
if (closed)
|
|
146
|
+
return;
|
|
147
|
+
// Ready with no rows, so preload() settles; the error is in state.
|
|
148
|
+
markReady();
|
|
149
|
+
publish({ error });
|
|
150
|
+
waiting.forEach((w) => w.fail(error));
|
|
151
|
+
waiting.clear();
|
|
152
|
+
},
|
|
153
|
+
settle: () => {
|
|
154
|
+
publish({ fetching: false });
|
|
155
|
+
schedule();
|
|
156
|
+
},
|
|
157
|
+
});
|
|
174
158
|
live = { load, apply, schedule };
|
|
175
159
|
void load();
|
|
176
160
|
return () => {
|
|
@@ -242,41 +226,29 @@ export function fetchCollectionOptions(options) {
|
|
|
242
226
|
};
|
|
243
227
|
}
|
|
244
228
|
// Fetched collections by owner and key, so every reader of a key shares one
|
|
245
|
-
// collection (and one request). One leaves
|
|
246
|
-
// after nothing has read it for a while.
|
|
247
|
-
const made =
|
|
229
|
+
// collection (and one request). One leaves the cache when TanStack DB cleans
|
|
230
|
+
// it up, after nothing has read it for a while.
|
|
231
|
+
const made = cacheOf();
|
|
248
232
|
/**
|
|
249
233
|
* The fetched collection for `options.key` under `owner` (a client, say),
|
|
250
234
|
* made once and shared. For a list pass `getKey`; leave it out for one object.
|
|
251
235
|
*/
|
|
252
236
|
export function fetched(owner, options) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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);
|
|
237
|
+
return made(owner, JSON.stringify(options.key), () => {
|
|
238
|
+
const collection = Object.assign(createCollection(fetchCollectionOptions(options)), { kind: options.getKey ? "list" : "one", key: options.key });
|
|
239
|
+
// Asking for the data (or putting some in) starts the sync if nothing
|
|
240
|
+
// has yet, so a route loader can await ready() on its own.
|
|
241
|
+
const { ready, set } = collection.utils;
|
|
242
|
+
collection.utils.ready = () => {
|
|
243
|
+
collection.startSyncImmediate();
|
|
244
|
+
return ready();
|
|
245
|
+
};
|
|
246
|
+
collection.utils.set = (data) => {
|
|
247
|
+
collection.startSyncImmediate();
|
|
248
|
+
set(data);
|
|
249
|
+
};
|
|
250
|
+
return collection;
|
|
277
251
|
});
|
|
278
|
-
byKey.set(hash, collection);
|
|
279
|
-
return collection;
|
|
280
252
|
}
|
|
281
253
|
// True when `key` starts with every part of `prefix`.
|
|
282
254
|
function startsWith(key, prefix) {
|
|
@@ -289,7 +261,7 @@ function startsWith(key, prefix) {
|
|
|
289
261
|
*/
|
|
290
262
|
export function refetchFetched(owner, prefix) {
|
|
291
263
|
const loads = [];
|
|
292
|
-
for (const collection of made.
|
|
264
|
+
for (const collection of made.of(owner)?.values() ?? []) {
|
|
293
265
|
if (!startsWith(collection.key, prefix))
|
|
294
266
|
continue;
|
|
295
267
|
loads.push(collection.utils.stale());
|
|
@@ -301,7 +273,7 @@ export function refetchFetched(owner, prefix) {
|
|
|
301
273
|
* `prefix`, so nothing shows its data again (after a project is deleted).
|
|
302
274
|
*/
|
|
303
275
|
export function forgetFetched(owner, prefix) {
|
|
304
|
-
const byKey = made.
|
|
276
|
+
const byKey = made.of(owner);
|
|
305
277
|
for (const [hash, collection] of byKey ?? []) {
|
|
306
278
|
if (!startsWith(collection.key, prefix))
|
|
307
279
|
continue;
|