@faybl/react-utils 1.1.0 → 1.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
@@ -6,23 +6,36 @@ React hooks for Faybl web apps.
6
6
  npm install @faybl/react-utils
7
7
  ```
8
8
 
9
- Every hook takes the faybl-client as `client` and does nothing until it has
10
- one. A list of rids or ids is compared by its content, so a new array with the
11
- same ids does not resubscribe.
9
+ Every hook takes the `@faybl/client` client as `client` and does nothing until
10
+ it has one. `useConnection` and `useSession` need client 4.1 or later. A list
11
+ of rids or ids is compared by its content, so a new array with the same ids
12
+ does not resubscribe.
12
13
 
13
14
  ```js
14
- import { useChannelsLatest, useChannelsSeries, usePresence } from '@faybl/react-utils';
15
+ import {
16
+ useChannelsLatest,
17
+ useChannelsSeries,
18
+ useConnection,
19
+ usePresence,
20
+ useSession,
21
+ } from '@faybl/react-utils';
15
22
 
16
23
  const presenceById = usePresence({ client, identityIds });
17
24
  const latest = useChannelsLatest({ client, rids, commitMs: 1000, onError });
18
25
  const series = useChannelsSeries({ client, rids, windowSeconds: 3600 });
26
+ const connected = useConnection({ client });
27
+ const { expired, login } = useSession({ client });
19
28
  ```
20
29
 
21
- | Hook | Returns |
22
- | ------------------- | ---------------------------------------------------------------------- |
23
- | `usePresence` | `{ [identityId]: { ts, connected } }`, live |
24
- | `useChannelsLatest` | `{ [rid]: { ts, pl } }`, the last stored point, then live |
25
- | `useChannelsSeries` | `{ [rid]: points }`, the last `windowSeconds`, oldest first, then live |
30
+ | Hook | Returns |
31
+ | ------------------- | ------------------------------------------------------------------------- |
32
+ | `usePresence` | `{ [identityId]: { ts, connected } }`, live |
33
+ | `useChannelsLatest` | `{ [rid]: { ts, pl } }`, the last stored point, then live |
34
+ | `useChannelsSeries` | `{ [rid]: points }`, the last `windowSeconds`, oldest first, then live |
35
+ | `useConnection` | whether the MQTT connection is up |
36
+ | `useSession` | `{ expired, login }`: whether the login session is gone, and the way back |
37
+
38
+ `connected` and `expired` are `undefined` until the client has reported them.
26
39
 
27
40
  The two channel hooks take two options:
28
41
 
@@ -30,3 +43,23 @@ The two channel hooks take two options:
30
43
  Without it, every point renders as it arrives.
31
44
  - `onError(error, rid)` is called when a history read fails. The hooks report
32
45
  nothing on their own.
46
+
47
+ ## An expired session
48
+
49
+ `useSession` shows nothing and never navigates on its own; what happens when
50
+ the session expires is the app's choice. `login` navigates the whole page to
51
+ `/oauth2/start` and back to where the user was, which happens at once while the
52
+ identity provider still has a session.
53
+
54
+ A banner with a button that calls `login` keeps whatever the user was typing. A
55
+ page with nothing to lose can call `login` from an effect once `expired` is
56
+ true instead.
57
+
58
+ ```jsx
59
+ const SessionBanner = ({ client }) => {
60
+ const { expired, login } = useSession({ client });
61
+ return expired ? (
62
+ <Alert action={<Button onClick={login}>Log in</Button>}>Your session has expired.</Alert>
63
+ ) : undefined;
64
+ };
65
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faybl/react-utils",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "React hooks for Faybl web apps",
5
5
  "license": "TBD",
6
6
  "author": "Arcturitech",
@@ -21,10 +21,16 @@
21
21
  "directory": "packages/react-utils"
22
22
  },
23
23
  "peerDependencies": {
24
+ "@faybl/client": "^4.1.0",
24
25
  "react": "^19.2.0"
25
26
  },
27
+ "peerDependenciesMeta": {
28
+ "@faybl/client": {
29
+ "optional": true
30
+ }
31
+ },
26
32
  "dependencies": {
27
- "@faybl/utils": "^1.1.0"
33
+ "@faybl/utils": "^1.2.0"
28
34
  },
29
35
  "engines": {
30
36
  "node": ">=22"
package/src/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  export { useChannelsLatest } from './useChannelsLatest.js';
2
2
  export { useChannelsSeries } from './useChannelsSeries.js';
3
+ export { useConnection } from './useConnection.js';
3
4
  export { usePresence } from './usePresence.js';
5
+ export { useSession } from './useSession.js';
@@ -0,0 +1,19 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ /**
4
+ * Whether the client's MQTT connection is up: true or false once the client
5
+ * has reported it, undefined before that and without a client.
6
+ * @param {object} params
7
+ * @param {object} params.client faybl-client with MQTT
8
+ * @returns {boolean | undefined}
9
+ */
10
+ export const useConnection = ({ client }) => {
11
+ const [state, setState] = useState({ client: undefined, connected: undefined });
12
+
13
+ useEffect(() => {
14
+ if (!client) return undefined;
15
+ return client.connection.subscribe(({ connected }) => setState({ client, connected }));
16
+ }, [client]);
17
+
18
+ return state.client === client ? state.connected : undefined;
19
+ };
@@ -0,0 +1,31 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ const LOGIN_PATH = '/oauth2/start';
4
+
5
+ const navigateTo = (url) => window.location.assign(url);
6
+
7
+ // The login proxy sends the user back to `rd`; while the identity provider
8
+ // still has a session that happens at once.
9
+ export const loginUrl = (href) => `${LOGIN_PATH}?rd=${encodeURIComponent(href)}`;
10
+
11
+ /**
12
+ * Whether the login session behind the client has expired, and `login`, which
13
+ * sends the user to log in and back to the current page. `expired` is
14
+ * undefined until the client has reported it. Nothing navigates on its own.
15
+ * @param {object} params
16
+ * @param {object} params.client faybl-client
17
+ * @param {(url: string) => void} [params.navigate] Top-level navigation
18
+ * @returns {{ expired: boolean | undefined, login: () => void }}
19
+ */
20
+ export const useSession = ({ client, navigate = navigateTo }) => {
21
+ const [state, setState] = useState({ client: undefined, expired: undefined });
22
+
23
+ useEffect(() => {
24
+ if (!client) return undefined;
25
+ return client.session.subscribe(({ expired }) => setState({ client, expired }));
26
+ }, [client]);
27
+
28
+ const login = useCallback(() => navigate(loginUrl(window.location.href)), [navigate]);
29
+
30
+ return { expired: state.client === client ? state.expired : undefined, login };
31
+ };