@velajs/react 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kauan Guesser
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,11 @@
1
+ import { createElement } from 'react';
2
+ import type { ReactNode } from 'react';
3
+ import type { LiveClient } from '@velajs/client';
4
+ import type { LiveContract } from '@velajs/client';
5
+ export interface LiveProviderProps {
6
+ client: LiveClient<LiveContract>;
7
+ children?: ReactNode;
8
+ }
9
+ /** Provides the LiveClient to the hook tree. `createElement`-based — no JSX toolchain required. */
10
+ export declare function LiveProvider(props: LiveProviderProps): ReturnType<typeof createElement>;
11
+ export declare function useLiveClient<C extends LiveContract = LiveContract>(): LiveClient<C>;
@@ -0,0 +1,17 @@
1
+ import { createContext, createElement, useContext } from "react";
2
+ // Deliberately `any`-typed inside the context: the contract generic is
3
+ // re-applied at the useLiveClient() boundary. One provider serves any app.
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ const LiveClientContext = createContext(null);
6
+ /** Provides the LiveClient to the hook tree. `createElement`-based — no JSX toolchain required. */ export function LiveProvider(props) {
7
+ return createElement(LiveClientContext.Provider, {
8
+ value: props.client
9
+ }, props.children);
10
+ }
11
+ export function useLiveClient() {
12
+ const client = useContext(LiveClientContext);
13
+ if (!client) {
14
+ throw new Error('useLiveClient: no LiveClient in context — wrap the tree in <LiveProvider client={…}>.');
15
+ }
16
+ return client;
17
+ }
@@ -0,0 +1,9 @@
1
+ export { LiveProvider, useLiveClient } from './context';
2
+ export type { LiveProviderProps } from './context';
3
+ export { useLiveQuery } from './use-live-query';
4
+ export type { UseLiveQueryOptions } from './use-live-query';
5
+ export { useLiveMutation } from './use-live-mutation';
6
+ export type { UseLiveMutationResult } from './use-live-mutation';
7
+ export { useConnectionStatus } from './use-connection-status';
8
+ export { usePresence } from './use-presence';
9
+ export type { UsePresenceOptions } from './use-presence';
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { LiveProvider, useLiveClient } from "./context.js";
2
+ export { useLiveQuery } from "./use-live-query.js";
3
+ export { useLiveMutation } from "./use-live-mutation.js";
4
+ export { useConnectionStatus } from "./use-connection-status.js";
5
+ export { usePresence } from "./use-presence.js";
@@ -0,0 +1,3 @@
1
+ import type { ConnectionStatus } from '@velajs/client';
2
+ /** The client's aggregate connection status, live. */
3
+ export declare function useConnectionStatus(): ConnectionStatus;
@@ -0,0 +1,12 @@
1
+ import { useCallback, useSyncExternalStore } from "react";
2
+ import { useLiveClient } from "./context.js";
3
+ /** The client's aggregate connection status, live. */ export function useConnectionStatus() {
4
+ const client = useLiveClient();
5
+ const subscribe = useCallback((onStoreChange)=>client.onConnectionStatus(()=>onStoreChange()), [
6
+ client
7
+ ]);
8
+ const getSnapshot = useCallback(()=>client.connectionStatus(), [
9
+ client
10
+ ]);
11
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
12
+ }
@@ -0,0 +1,23 @@
1
+ import type { MutateOptions } from '@velajs/client';
2
+ export interface UseLiveMutationResult<R> {
3
+ /** Fire the mutation. Per-call options (optimistic targets, method, …) merge over the hook defaults. */
4
+ mutate: (body?: unknown, options?: MutateOptions) => Promise<R>;
5
+ pending: boolean;
6
+ data: R | undefined;
7
+ error: unknown;
8
+ reset: () => void;
9
+ }
10
+ /**
11
+ * HTTP mutation hook with live-aware optimistic updates:
12
+ *
13
+ * ```tsx
14
+ * const { mutate: addTodo, pending } = useLiveMutation('/todos', {
15
+ * optimistic: { query: 'todos.list', args: { listId }, apply: (t = []) => [...t, temp] },
16
+ * });
17
+ * ```
18
+ *
19
+ * The optimistic layer is dropped exactly when a live frame's cursor passes
20
+ * the mutation's `Vela-Commit-Cursor` (see @velajs/client); failures roll it
21
+ * back and reject.
22
+ */
23
+ export declare function useLiveMutation<R = unknown>(path: string, defaults?: MutateOptions): UseLiveMutationResult<R>;
@@ -0,0 +1,59 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ import { useLiveClient } from "./context.js";
3
+ /**
4
+ * HTTP mutation hook with live-aware optimistic updates:
5
+ *
6
+ * ```tsx
7
+ * const { mutate: addTodo, pending } = useLiveMutation('/todos', {
8
+ * optimistic: { query: 'todos.list', args: { listId }, apply: (t = []) => [...t, temp] },
9
+ * });
10
+ * ```
11
+ *
12
+ * The optimistic layer is dropped exactly when a live frame's cursor passes
13
+ * the mutation's `Vela-Commit-Cursor` (see @velajs/client); failures roll it
14
+ * back and reject.
15
+ */ export function useLiveMutation(path, defaults) {
16
+ const client = useLiveClient();
17
+ const [state, setState] = useState({
18
+ pending: 0
19
+ });
20
+ const defaultsRef = useRef(defaults);
21
+ defaultsRef.current = defaults;
22
+ const mutate = useCallback(async (body, options)=>{
23
+ setState((current)=>({
24
+ ...current,
25
+ pending: current.pending + 1
26
+ }));
27
+ try {
28
+ const data = await client.mutate(path, body, {
29
+ ...defaultsRef.current,
30
+ ...options
31
+ });
32
+ setState((current)=>({
33
+ pending: current.pending - 1,
34
+ data,
35
+ error: undefined
36
+ }));
37
+ return data;
38
+ } catch (error) {
39
+ setState((current)=>({
40
+ ...current,
41
+ pending: current.pending - 1,
42
+ error
43
+ }));
44
+ throw error;
45
+ }
46
+ }, [
47
+ client,
48
+ path
49
+ ]);
50
+ return {
51
+ mutate,
52
+ pending: state.pending > 0,
53
+ data: state.data,
54
+ error: state.error,
55
+ reset: useCallback(()=>setState({
56
+ pending: 0
57
+ }), [])
58
+ };
59
+ }
@@ -0,0 +1,17 @@
1
+ import type { ArgsOf, LiveContract, ResultOf, SubscribeOptions } from '@velajs/client';
2
+ export interface UseLiveQueryOptions extends SubscribeOptions {
3
+ /** Render without subscribing (conditional queries). */
4
+ skip?: boolean;
5
+ }
6
+ /**
7
+ * Subscribe to a live query. Returns `undefined` until the first
8
+ * snapshot/hydrated value, then re-renders on every server push. Built on
9
+ * `useSyncExternalStore` — the client guarantees referentially stable
10
+ * snapshots between notifications, so there is no tearing and no external
11
+ * cache library.
12
+ *
13
+ * ```tsx
14
+ * const todos = useLiveQuery('todos.list', { listId }) ?? [];
15
+ * ```
16
+ */
17
+ export declare function useLiveQuery<C extends LiveContract, Q extends keyof C & string>(query: Q, args: ArgsOf<C, Q>, options?: UseLiveQueryOptions): ResultOf<C, Q> | undefined;
@@ -0,0 +1,46 @@
1
+ import { useCallback, useMemo, useSyncExternalStore } from "react";
2
+ import { argsKeyOf } from "@velajs/client";
3
+ import { useLiveClient } from "./context.js";
4
+ /**
5
+ * Subscribe to a live query. Returns `undefined` until the first
6
+ * snapshot/hydrated value, then re-renders on every server push. Built on
7
+ * `useSyncExternalStore` — the client guarantees referentially stable
8
+ * snapshots between notifications, so there is no tearing and no external
9
+ * cache library.
10
+ *
11
+ * ```tsx
12
+ * const todos = useLiveQuery('todos.list', { listId }) ?? [];
13
+ * ```
14
+ */ export function useLiveQuery(query, args, options) {
15
+ const client = useLiveClient();
16
+ // Stable identity for the deps array — callers pass fresh object literals.
17
+ const argsKey = useMemo(()=>argsKeyOf(args), [
18
+ args
19
+ ]);
20
+ const { room, key, skip, onError } = options ?? {};
21
+ const subscribe = useCallback((onStoreChange)=>{
22
+ if (skip) return ()=>{};
23
+ return client.subscribe(query, args, ()=>onStoreChange(), {
24
+ room,
25
+ key,
26
+ onError
27
+ });
28
+ }, // eslint-disable-next-line react-hooks/exhaustive-deps -- argsKey stands in for args; onError is deliberately unbound
29
+ [
30
+ client,
31
+ query,
32
+ argsKey,
33
+ room,
34
+ key,
35
+ skip
36
+ ]);
37
+ const getSnapshot = useCallback(()=>skip ? undefined : client.peek(query, args, room), // eslint-disable-next-line react-hooks/exhaustive-deps -- argsKey stands in for args
38
+ [
39
+ client,
40
+ query,
41
+ argsKey,
42
+ room,
43
+ skip
44
+ ]);
45
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
46
+ }
@@ -0,0 +1,17 @@
1
+ import type { PresenceMember } from '@velajs/client/presence';
2
+ export interface UsePresenceOptions {
3
+ /** Payload attached to this connection's roster entry (latest render's value rides each beat). */
4
+ meta?: unknown;
5
+ /** Keep well under the server TTL (default 10 000 ms vs 30 000 ms). */
6
+ heartbeatIntervalMs?: number;
7
+ }
8
+ /**
9
+ * Join a room's presence and observe its roster. Heartbeats ride the room's
10
+ * live socket; departure is immediate on close; a tab regaining visibility
11
+ * beats right away so the roster recovers from a throttled background timer.
12
+ *
13
+ * ```tsx
14
+ * const people = usePresence(roomId, { meta: { name: user.name } });
15
+ * ```
16
+ */
17
+ export declare function usePresence(room: string, options?: UsePresenceOptions): PresenceMember[];
@@ -0,0 +1,41 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { createPresence } from "@velajs/client/presence";
3
+ import { useLiveClient } from "./context.js";
4
+ /**
5
+ * Join a room's presence and observe its roster. Heartbeats ride the room's
6
+ * live socket; departure is immediate on close; a tab regaining visibility
7
+ * beats right away so the roster recovers from a throttled background timer.
8
+ *
9
+ * ```tsx
10
+ * const people = usePresence(roomId, { meta: { name: user.name } });
11
+ * ```
12
+ */ export function usePresence(room, options) {
13
+ const client = useLiveClient();
14
+ const [members, setMembers] = useState([]);
15
+ const metaRef = useRef(options?.meta);
16
+ metaRef.current = options?.meta;
17
+ const heartbeatIntervalMs = options?.heartbeatIntervalMs;
18
+ useEffect(()=>{
19
+ const handle = createPresence(client, {
20
+ room,
21
+ meta: ()=>metaRef.current,
22
+ heartbeatIntervalMs,
23
+ onRoster: setMembers
24
+ });
25
+ const doc = globalThis.document;
26
+ const onVisibility = ()=>{
27
+ if (doc?.visibilityState === 'visible') handle.beat();
28
+ };
29
+ doc?.addEventListener?.('visibilitychange', onVisibility);
30
+ return ()=>{
31
+ doc?.removeEventListener?.('visibilitychange', onVisibility);
32
+ handle.stop();
33
+ setMembers([]);
34
+ };
35
+ }, [
36
+ client,
37
+ room,
38
+ heartbeatIntervalMs
39
+ ]);
40
+ return members;
41
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@velajs/react",
3
+ "version": "0.1.0",
4
+ "description": "React hooks for Vela live queries: useLiveQuery, useLiveMutation, usePresence, useConnectionStatus on useSyncExternalStore",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE",
18
+ "CHANGELOG.md"
19
+ ],
20
+ "sideEffects": false,
21
+ "keywords": [
22
+ "vela",
23
+ "react",
24
+ "hooks",
25
+ "live-queries",
26
+ "realtime",
27
+ "optimistic-updates"
28
+ ],
29
+ "author": "ksh",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/velajs/client.git",
34
+ "directory": "packages/react"
35
+ },
36
+ "homepage": "https://github.com/velajs/client#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/velajs/client/issues"
39
+ },
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "dependencies": {
44
+ "@velajs/client": "^0.1.0"
45
+ },
46
+ "peerDependencies": {
47
+ "react": ">=18"
48
+ },
49
+ "devDependencies": {
50
+ "@swc/cli": "^0.8.1",
51
+ "@swc/core": "^1.15.43",
52
+ "@testing-library/react": "^16.3.0",
53
+ "@types/react": "^19.0.0",
54
+ "happy-dom": "^20.0.0",
55
+ "react": "^19.0.0",
56
+ "react-dom": "^19.0.0",
57
+ "typescript": "^6.0.3",
58
+ "unplugin-swc": "^1.5.9",
59
+ "vitest": "^4.1.9"
60
+ },
61
+ "scripts": {
62
+ "build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
63
+ "test": "vitest run",
64
+ "typecheck": "tsc --noEmit"
65
+ }
66
+ }