@polyphron/viper-react 0.4.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 CHANGED
@@ -90,12 +90,11 @@ to refresh the email shown by `useUser()`.
90
90
  | `useAuth(collection)` | `{ user, auth }` for an auth collection |
91
91
  | `useSession()` / `useUser()` | The session / the logged-in user, re-rendering on change |
92
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, sub-app branding), or `null` while it loads |
93
+ | `useAuthMethods({ collection?, enabled? })` | How to sign in here (methods, sign-up, project branding), or `null` while it loads |
94
94
  | `useSessions()` | The user's active sessions: `{ sessions, error, loading, refresh, revoke, logoutEverywhere }` |
95
95
  | `useInvites()` | Invites the user sent: `{ invites, error, loading, refresh, invite, cancel }` |
96
96
  | `useTenants(name)` | The user's memberships of tenant collection `name`; never the last user's list |
97
97
  | `useRun(name, started, { every? })` | Follows a background workflow's run until it ends: `{ data, error, pending }` |
98
- | `useAppIdentity()` / `useAppManagement()` | The user's app role and ceilings; opt-in role and member edits. Both give `{ identity, error, loading, refresh }`, management adds `roles`, `members` and the writes. A 403 on the management calls keeps `identity` |
99
98
  | `useRealtime(topics, onEvent)` | Watch changes yourself; returns the connection status |
100
99
  | `useRealtimeStatus()` | `"closed"`, `"connecting"` or `"live"` |
101
100
 
@@ -113,7 +112,7 @@ flight, so lists don't flash.
113
112
  - With `live: true`, the list is also re-fetched once after the socket comes
114
113
  back from a drop, since events may have been missed while it was down.
115
114
 
116
- `useSessions`, `useInvites` and `useAppIdentity` all give `loading` (true
115
+ `useSessions` and `useInvites` both give `loading` (true
117
116
  until the first answer for this user), `error` (an `Error` or `null`) and
118
117
  `refresh()`, which resolves once the new answer is in. They key their state on
119
118
  the signed-in user, so after a user switch you never see the last user's data.
package/dist/auth.d.ts CHANGED
@@ -50,15 +50,15 @@ export declare function useAuth<T extends RecordModel = RecordModel>(collection?
50
50
  auth: AuthService<T>;
51
51
  };
52
52
  /**
53
- * How to sign in here: the ways on offer, sign-up, and the sub-app's name
54
- * and branding when the client has one (`new Viper({ app })`). Null while
53
+ * How to sign in here: the ways on offer, sign-up, and the project's
54
+ * branding. Null while
55
55
  * it loads, and when the load fails: the sign-in calls report their own
56
56
  * errors. Name an auth `collection` when the project has several, and pass
57
57
  * `enabled: false` to hold the request back. For a sign-in page.
58
58
  *
59
59
  * ```tsx
60
60
  * const methods = useAuthMethods()
61
- * return <h1>{methods?.app?.branding.name ?? methods?.app?.name}</h1>
61
+ * return <h1>{methods?.branding?.name}</h1>
62
62
  * ```
63
63
  */
64
64
  export declare function useAuthMethods(options?: {
package/dist/auth.js CHANGED
@@ -81,15 +81,15 @@ export function useAuth(collection) {
81
81
  return { user, auth };
82
82
  }
83
83
  /**
84
- * How to sign in here: the ways on offer, sign-up, and the sub-app's name
85
- * and branding when the client has one (`new Viper({ app })`). Null while
84
+ * How to sign in here: the ways on offer, sign-up, and the project's
85
+ * branding. Null while
86
86
  * it loads, and when the load fails: the sign-in calls report their own
87
87
  * errors. Name an auth `collection` when the project has several, and pass
88
88
  * `enabled: false` to hold the request back. For a sign-in page.
89
89
  *
90
90
  * ```tsx
91
91
  * const methods = useAuthMethods()
92
- * return <h1>{methods?.app?.branding.name ?? methods?.app?.name}</h1>
92
+ * return <h1>{methods?.branding?.name}</h1>
93
93
  * ```
94
94
  */
95
95
  export function useAuthMethods(options = {}) {
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { ViperProvider, useViper, type ViperProviderProps } from "./provider.js";
2
2
  export { useAuth, useAuthMethods, useSession, useSessionOf, useInvites, useSessions, useTenants, useUser, } from "./auth.js";
3
- export { useRealtime, useRealtimeStatus } from "./realtime.js";
3
+ export { usePresence, useRealtime, useRealtimeStatus } from "./realtime.js";
4
4
  export { useLoaded, type Query } from "./loaded.js";
5
- export { useRecord, useRecords, type RecordsOptions } from "./records.js";
5
+ export { useAggregate, useRecord, useRecords, type AggregateHookOptions, type RecordsOptions, } from "./records.js";
6
+ export { useUpload, type UploadState } from "./upload.js";
6
7
  export { useRun, type RunState } from "./runs.js";
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";
8
+ export { Viper, ViperError, fieldError, memoryStorage, type AggregateOptions, type AggregateResult, type AggregateRow, type ListOptions, type ListResult, type PresenceMember, type RealtimeEvent, type RealtimeStatus, type Room, type RoomListener, type RecordInput, type RecordModel, type Session, type UserSession, type StartedRun, type ViperOptions, type WorkflowRun, } from "@polyphron/viper-client";
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  export { ViperProvider, useViper } from "./provider.js";
2
2
  export { useAuth, useAuthMethods, useSession, useSessionOf, useInvites, useSessions, useTenants, useUser, } from "./auth.js";
3
- export { useRealtime, useRealtimeStatus } from "./realtime.js";
3
+ export { usePresence, useRealtime, useRealtimeStatus } from "./realtime.js";
4
4
  export { useLoaded } from "./loaded.js";
5
- export { useRecord, useRecords } from "./records.js";
5
+ export { useAggregate, useRecord, useRecords, } from "./records.js";
6
+ export { useUpload } from "./upload.js";
6
7
  export { useRun } from "./runs.js";
7
- export { useAppIdentity, useAppManagement } from "./identity.js";
8
8
  // Re-exported so an app only needs @polyphron/viper-react in its imports.
9
9
  export { Viper, ViperError, fieldError, memoryStorage, } from "@polyphron/viper-client";
@@ -1,4 +1,4 @@
1
- import type { RealtimeEvent, RealtimeStatus, RecordModel } from "@polyphron/viper-client";
1
+ import type { PresenceMember, RealtimeEvent, RealtimeStatus, RecordModel } from "@polyphron/viper-client";
2
2
  /**
3
3
  * Watches topics and calls `onEvent` for each change. A topic is a collection
4
4
  * name for every record in it (`posts`), or `collection/id` for one record.
@@ -8,3 +8,14 @@ import type { RealtimeEvent, RealtimeStatus, RecordModel } from "@polyphron/vipe
8
8
  export declare function useRealtime<T extends RecordModel = RecordModel>(topics: string | string[], onEvent: (event: RealtimeEvent<T>) => void): RealtimeStatus;
9
9
  /** The realtime connection status, on its own. */
10
10
  export declare function useRealtimeStatus(): RealtimeStatus;
11
+ /**
12
+ * Joins a room (`posts` or `posts/abc123`) while mounted and returns who is
13
+ * in it. `state` is what others see about you; a new value is sent when its
14
+ * JSON changes. `onBroadcast` hears what the others send with `broadcast`.
15
+ */
16
+ export declare function usePresence<S = unknown, D = unknown>(room: string, state?: S, onBroadcast?: (data: D, from: PresenceMember<S>) => void): {
17
+ members: PresenceMember<S>[];
18
+ me: string;
19
+ error: string;
20
+ broadcast: (data: D) => void;
21
+ };
package/dist/realtime.js CHANGED
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef, useSyncExternalStore } from "react";
1
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
2
2
  import { useViper } from "./provider.js";
3
3
  /**
4
4
  * Watches topics and calls `onEvent` for each change. A topic is a collection
@@ -23,3 +23,52 @@ export function useRealtimeStatus() {
23
23
  const viper = useViper();
24
24
  return useSyncExternalStore((listener) => viper.realtime.onStatus(listener), () => viper.realtime.status, () => "closed");
25
25
  }
26
+ /**
27
+ * Joins a room (`posts` or `posts/abc123`) while mounted and returns who is
28
+ * in it. `state` is what others see about you; a new value is sent when its
29
+ * JSON changes. `onBroadcast` hears what the others send with `broadcast`.
30
+ */
31
+ export function usePresence(room, state, onBroadcast) {
32
+ const viper = useViper();
33
+ const [members, setMembers] = useState([]);
34
+ const [error, setError] = useState("");
35
+ const [joined, setJoined] = useState(null);
36
+ const onBroadcastRef = useRef(onBroadcast);
37
+ useEffect(() => {
38
+ onBroadcastRef.current = onBroadcast;
39
+ });
40
+ // The state as JSON, so a new object with the same values sends nothing.
41
+ const stateKey = JSON.stringify(state ?? null);
42
+ const stateRef = useRef(state);
43
+ useEffect(() => {
44
+ stateRef.current = state;
45
+ });
46
+ useEffect(() => {
47
+ if (!room)
48
+ return;
49
+ const r = viper.realtime.join(room, {
50
+ onPresence: (m) => {
51
+ setMembers(m);
52
+ setError("");
53
+ },
54
+ onBroadcast: (data, from) => onBroadcastRef.current?.(data, from),
55
+ onError: setError,
56
+ }, stateRef.current);
57
+ setJoined(r);
58
+ return () => {
59
+ r.leave();
60
+ setJoined(null);
61
+ setMembers([]);
62
+ };
63
+ }, [viper, room]);
64
+ useEffect(() => {
65
+ if (joined && stateRef.current !== undefined)
66
+ joined.track(stateRef.current);
67
+ }, [joined, stateKey]);
68
+ return {
69
+ members,
70
+ me: joined?.me ?? "",
71
+ error,
72
+ broadcast: (data) => joined?.broadcast(data),
73
+ };
74
+ }
package/dist/records.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ListOptions, ListResult, RecordModel } from "@polyphron/viper-client";
1
+ import type { AggregateOptions, AggregateResult, ListOptions, ListResult, RecordModel } from "@polyphron/viper-client";
2
2
  import { type Query } from "./loaded.js";
3
3
  export type RecordsOptions = ListOptions & {
4
4
  /** Re-fetch when anything in the collection changes. Off by default. */
@@ -12,8 +12,22 @@ export type RecordsOptions = ListOptions & {
12
12
  * ```
13
13
  */
14
14
  export declare function useRecords<T extends RecordModel = RecordModel>(collection: string, options?: RecordsOptions): Query<ListResult<T>>;
15
+ export type AggregateHookOptions = AggregateOptions & {
16
+ /** Re-fetch when anything in the collection changes. Off by default. */
17
+ live?: boolean;
18
+ };
19
+ /**
20
+ * Counts and sums of a collection, grouped by fields or time buckets,
21
+ * re-fetched when the options change.
22
+ *
23
+ * ```tsx
24
+ * const { data } = useAggregate("orders", { group: "created:day", agg: "count,sum:amount" })
25
+ * ```
26
+ */
27
+ export declare function useAggregate(collection: string, options?: AggregateHookOptions): Query<AggregateResult>;
15
28
  /** One record, re-fetched when the id changes. */
16
29
  export declare function useRecord<T extends RecordModel = RecordModel>(collection: string, id: string | undefined, options?: {
17
30
  expand?: string;
31
+ fields?: string;
18
32
  live?: boolean;
19
33
  }): Query<T>;
package/dist/records.js CHANGED
@@ -20,15 +20,34 @@ export function useRecords(collection, options = {}) {
20
20
  useLiveReload(live, collection, query.reload, viper);
21
21
  return query;
22
22
  }
23
+ /**
24
+ * Counts and sums of a collection, grouped by fields or time buckets,
25
+ * re-fetched when the options change.
26
+ *
27
+ * ```tsx
28
+ * const { data } = useAggregate("orders", { group: "created:day", agg: "count,sum:amount" })
29
+ * ```
30
+ */
31
+ export function useAggregate(collection, options = {}) {
32
+ const viper = useViper();
33
+ const { live = false, ...aggregate } = options;
34
+ // Options are a fresh object every render; compare them by value.
35
+ const key = JSON.stringify(aggregate);
36
+ const query = useLoaded(`${collection}:aggregate`, useCallback((signal) => viper
37
+ .collection(collection)
38
+ .aggregate({ ...JSON.parse(key), signal }), [viper, collection, key]));
39
+ useLiveReload(live, collection, query.reload, viper);
40
+ return query;
41
+ }
23
42
  /** One record, re-fetched when the id changes. */
24
43
  export function useRecord(collection, id, options = {}) {
25
44
  const viper = useViper();
26
- const { live = false, expand } = options;
45
+ const { live = false, expand, fields } = options;
27
46
  const query = useLoaded(
28
47
  // A different record must never show the last one's data.
29
48
  `${collection}/${id ?? ""}`, useCallback((signal) => id
30
- ? viper.collection(collection).view(id, { expand, signal })
31
- : Promise.resolve(null), [viper, collection, id, expand]));
49
+ ? viper.collection(collection).view(id, { expand, fields, signal })
50
+ : Promise.resolve(null), [viper, collection, id, expand, fields]));
32
51
  useLiveReload(live && Boolean(id), `${collection}/${id}`, query.reload, viper);
33
52
  return query;
34
53
  }
@@ -0,0 +1,24 @@
1
+ import type { RecordModel, UploadLargeOptions, ViperError } from "@polyphron/viper-client";
2
+ /** Where a big upload is: what `useUpload` returns next to `upload`. */
3
+ export type UploadState<T extends RecordModel> = {
4
+ /** Sends the file; resolves with the record, rejects on failure. */
5
+ upload: (id: string, field: string, file: Blob, options?: Omit<UploadLargeOptions, "onProgress" | "signal">) => Promise<T>;
6
+ /** Stops the upload in flight. */
7
+ cancel: () => void;
8
+ /** 0 to 1; stays at 1 once done. */
9
+ progress: number;
10
+ uploading: boolean;
11
+ error: ViperError | Error | null;
12
+ /** The record the last finished upload returned. */
13
+ data: T | null;
14
+ };
15
+ /**
16
+ * A big file into a file field, in parts or straight to the bucket (the
17
+ * project's big-uploads switch must be on), with progress.
18
+ *
19
+ * ```tsx
20
+ * const { upload, progress } = useUpload("videos")
21
+ * await upload(record.id, "file", input.files[0])
22
+ * ```
23
+ */
24
+ export declare function useUpload<T extends RecordModel = RecordModel>(collection: string): UploadState<T>;
package/dist/upload.js ADDED
@@ -0,0 +1,44 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ import { useViper } from "./provider.js";
3
+ /**
4
+ * A big file into a file field, in parts or straight to the bucket (the
5
+ * project's big-uploads switch must be on), with progress.
6
+ *
7
+ * ```tsx
8
+ * const { upload, progress } = useUpload("videos")
9
+ * await upload(record.id, "file", input.files[0])
10
+ * ```
11
+ */
12
+ export function useUpload(collection) {
13
+ const viper = useViper();
14
+ const [progress, setProgress] = useState(0);
15
+ const [uploading, setUploading] = useState(false);
16
+ const [error, setError] = useState(null);
17
+ const [data, setData] = useState(null);
18
+ const abort = useRef(null);
19
+ const upload = useCallback(async (id, field, file, options = {}) => {
20
+ abort.current = new AbortController();
21
+ setUploading(true);
22
+ setProgress(0);
23
+ setError(null);
24
+ try {
25
+ const record = await viper.collection(collection).uploadLarge(id, field, file, {
26
+ ...options,
27
+ signal: abort.current.signal,
28
+ onProgress: (sent, total) => setProgress(total ? sent / total : 1),
29
+ });
30
+ setProgress(1);
31
+ setData(record);
32
+ return record;
33
+ }
34
+ catch (e) {
35
+ setError(e);
36
+ throw e;
37
+ }
38
+ finally {
39
+ setUploading(false);
40
+ }
41
+ }, [viper, collection]);
42
+ const cancel = useCallback(() => abort.current?.abort(), []);
43
+ return { upload, cancel, progress, uploading, error, data };
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polyphron/viper-react",
3
- "version": "0.4.0",
3
+ "version": "0.5.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.4.0",
33
+ "@polyphron/viper-client": "^0.5.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"