@velajs/react 0.1.0 → 0.1.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @velajs/react
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2419688: Modernize the package build, validation, and release toolchain.
8
+ - Updated dependencies [2419688]
9
+ - @velajs/client@0.1.1
package/dist/index.d.ts CHANGED
@@ -1,9 +1,78 @@
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';
1
+ import { ReactNode, createElement } from "react";
2
+ import { ArgsOf, ConnectionStatus, LiveClient, LiveContract, MutateOptions, ResultOf, SubscribeOptions } from "@velajs/client";
3
+ import { PresenceMember } from "@velajs/client/presence";
4
+ //#region src/context.d.ts
5
+ 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
+ declare function LiveProvider(props: LiveProviderProps): ReturnType<typeof createElement>;
11
+ declare function useLiveClient<C extends LiveContract = LiveContract>(): LiveClient<C>;
12
+ //#endregion
13
+ //#region src/use-live-query.d.ts
14
+ interface UseLiveQueryOptions extends SubscribeOptions {
15
+ /** Render without subscribing (conditional queries). */
16
+ skip?: boolean;
17
+ }
18
+ /**
19
+ * Subscribe to a live query. Returns `undefined` until the first
20
+ * snapshot/hydrated value, then re-renders on every server push. Built on
21
+ * `useSyncExternalStore` — the client guarantees referentially stable
22
+ * snapshots between notifications, so there is no tearing and no external
23
+ * cache library.
24
+ *
25
+ * ```tsx
26
+ * const todos = useLiveQuery('todos.list', { listId }) ?? [];
27
+ * ```
28
+ */
29
+ declare function useLiveQuery<C extends LiveContract, Q extends keyof C & string>(query: Q, args: ArgsOf<C, Q>, options?: UseLiveQueryOptions): ResultOf<C, Q> | undefined;
30
+ //#endregion
31
+ //#region src/use-live-mutation.d.ts
32
+ interface UseLiveMutationResult<R> {
33
+ /** Fire the mutation. Per-call options (optimistic targets, method, …) merge over the hook defaults. */
34
+ mutate: (body?: unknown, options?: MutateOptions) => Promise<R>;
35
+ pending: boolean;
36
+ data: R | undefined;
37
+ error: unknown;
38
+ reset: () => void;
39
+ }
40
+ /**
41
+ * HTTP mutation hook with live-aware optimistic updates:
42
+ *
43
+ * ```tsx
44
+ * const { mutate: addTodo, pending } = useLiveMutation('/todos', {
45
+ * optimistic: { query: 'todos.list', args: { listId }, apply: (t = []) => [...t, temp] },
46
+ * });
47
+ * ```
48
+ *
49
+ * The optimistic layer is dropped exactly when a live frame's cursor passes
50
+ * the mutation's `Vela-Commit-Cursor` (see @velajs/client); failures roll it
51
+ * back and reject.
52
+ */
53
+ declare function useLiveMutation<R = unknown>(path: string, defaults?: MutateOptions): UseLiveMutationResult<R>;
54
+ //#endregion
55
+ //#region src/use-connection-status.d.ts
56
+ /** The client's aggregate connection status, live. */
57
+ declare function useConnectionStatus(): ConnectionStatus;
58
+ //#endregion
59
+ //#region src/use-presence.d.ts
60
+ interface UsePresenceOptions {
61
+ /** Payload attached to this connection's roster entry (latest render's value rides each beat). */
62
+ meta?: unknown;
63
+ /** Keep well under the server TTL (default 10 000 ms vs 30 000 ms). */
64
+ heartbeatIntervalMs?: number;
65
+ }
66
+ /**
67
+ * Join a room's presence and observe its roster. Heartbeats ride the room's
68
+ * live socket; departure is immediate on close; a tab regaining visibility
69
+ * beats right away so the roster recovers from a throttled background timer.
70
+ *
71
+ * ```tsx
72
+ * const people = usePresence(roomId, { meta: { name: user.name } });
73
+ * ```
74
+ */
75
+ declare function usePresence(room: string, options?: UsePresenceOptions): PresenceMember[];
76
+ //#endregion
77
+ export { LiveProvider, type LiveProviderProps, type UseLiveMutationResult, type UseLiveQueryOptions, type UsePresenceOptions, useConnectionStatus, useLiveClient, useLiveMutation, useLiveQuery, usePresence };
78
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,5 +1,161 @@
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";
1
+ import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { argsKeyOf } from "@velajs/client";
3
+ import { createPresence } from "@velajs/client/presence";
4
+ //#region src/context.ts
5
+ const LiveClientContext = createContext(null);
6
+ /** Provides the LiveClient to the hook tree. `createElement`-based — no JSX toolchain required. */
7
+ function LiveProvider(props) {
8
+ return createElement(LiveClientContext.Provider, { value: props.client }, props.children);
9
+ }
10
+ function useLiveClient() {
11
+ const client = useContext(LiveClientContext);
12
+ if (!client) throw new Error("useLiveClient: no LiveClient in context — wrap the tree in <LiveProvider client={…}>.");
13
+ return client;
14
+ }
15
+ //#endregion
16
+ //#region src/use-live-query.ts
17
+ /**
18
+ * Subscribe to a live query. Returns `undefined` until the first
19
+ * snapshot/hydrated value, then re-renders on every server push. Built on
20
+ * `useSyncExternalStore` — the client guarantees referentially stable
21
+ * snapshots between notifications, so there is no tearing and no external
22
+ * cache library.
23
+ *
24
+ * ```tsx
25
+ * const todos = useLiveQuery('todos.list', { listId }) ?? [];
26
+ * ```
27
+ */
28
+ function useLiveQuery(query, args, options) {
29
+ const client = useLiveClient();
30
+ const argsKey = useMemo(() => argsKeyOf(args), [args]);
31
+ const { room, key, skip, onError } = options ?? {};
32
+ const subscribe = useCallback((onStoreChange) => {
33
+ if (skip) return () => {};
34
+ return client.subscribe(query, args, () => onStoreChange(), {
35
+ room,
36
+ key,
37
+ onError
38
+ });
39
+ }, [
40
+ client,
41
+ query,
42
+ argsKey,
43
+ room,
44
+ key,
45
+ skip
46
+ ]);
47
+ const getSnapshot = useCallback(() => skip ? void 0 : client.peek(query, args, room), [
48
+ client,
49
+ query,
50
+ argsKey,
51
+ room,
52
+ skip
53
+ ]);
54
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
55
+ }
56
+ //#endregion
57
+ //#region src/use-live-mutation.ts
58
+ /**
59
+ * HTTP mutation hook with live-aware optimistic updates:
60
+ *
61
+ * ```tsx
62
+ * const { mutate: addTodo, pending } = useLiveMutation('/todos', {
63
+ * optimistic: { query: 'todos.list', args: { listId }, apply: (t = []) => [...t, temp] },
64
+ * });
65
+ * ```
66
+ *
67
+ * The optimistic layer is dropped exactly when a live frame's cursor passes
68
+ * the mutation's `Vela-Commit-Cursor` (see @velajs/client); failures roll it
69
+ * back and reject.
70
+ */
71
+ function useLiveMutation(path, defaults) {
72
+ const client = useLiveClient();
73
+ const [state, setState] = useState({ pending: 0 });
74
+ const defaultsRef = useRef(defaults);
75
+ defaultsRef.current = defaults;
76
+ return {
77
+ mutate: useCallback(async (body, options) => {
78
+ setState((current) => ({
79
+ ...current,
80
+ pending: current.pending + 1
81
+ }));
82
+ try {
83
+ const data = await client.mutate(path, body, {
84
+ ...defaultsRef.current,
85
+ ...options
86
+ });
87
+ setState((current) => ({
88
+ pending: current.pending - 1,
89
+ data,
90
+ error: void 0
91
+ }));
92
+ return data;
93
+ } catch (error) {
94
+ setState((current) => ({
95
+ ...current,
96
+ pending: current.pending - 1,
97
+ error
98
+ }));
99
+ throw error;
100
+ }
101
+ }, [client, path]),
102
+ pending: state.pending > 0,
103
+ data: state.data,
104
+ error: state.error,
105
+ reset: useCallback(() => setState({ pending: 0 }), [])
106
+ };
107
+ }
108
+ //#endregion
109
+ //#region src/use-connection-status.ts
110
+ /** The client's aggregate connection status, live. */
111
+ function useConnectionStatus() {
112
+ const client = useLiveClient();
113
+ const subscribe = useCallback((onStoreChange) => client.onConnectionStatus(() => onStoreChange()), [client]);
114
+ const getSnapshot = useCallback(() => client.connectionStatus(), [client]);
115
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
116
+ }
117
+ //#endregion
118
+ //#region src/use-presence.ts
119
+ /**
120
+ * Join a room's presence and observe its roster. Heartbeats ride the room's
121
+ * live socket; departure is immediate on close; a tab regaining visibility
122
+ * beats right away so the roster recovers from a throttled background timer.
123
+ *
124
+ * ```tsx
125
+ * const people = usePresence(roomId, { meta: { name: user.name } });
126
+ * ```
127
+ */
128
+ function usePresence(room, options) {
129
+ const client = useLiveClient();
130
+ const [members, setMembers] = useState([]);
131
+ const metaRef = useRef(options?.meta);
132
+ metaRef.current = options?.meta;
133
+ const heartbeatIntervalMs = options?.heartbeatIntervalMs;
134
+ useEffect(() => {
135
+ const handle = createPresence(client, {
136
+ room,
137
+ meta: () => metaRef.current,
138
+ heartbeatIntervalMs,
139
+ onRoster: setMembers
140
+ });
141
+ const doc = globalThis.document;
142
+ const onVisibility = () => {
143
+ if (doc?.visibilityState === "visible") handle.beat();
144
+ };
145
+ doc?.addEventListener?.("visibilitychange", onVisibility);
146
+ return () => {
147
+ doc?.removeEventListener?.("visibilitychange", onVisibility);
148
+ handle.stop();
149
+ setMembers([]);
150
+ };
151
+ }, [
152
+ client,
153
+ room,
154
+ heartbeatIntervalMs
155
+ ]);
156
+ return members;
157
+ }
158
+ //#endregion
159
+ export { LiveProvider, useConnectionStatus, useLiveClient, useLiveMutation, useLiveQuery, usePresence };
160
+
161
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/context.ts","../src/use-live-query.ts","../src/use-live-mutation.ts","../src/use-connection-status.ts","../src/use-presence.ts"],"sourcesContent":["import { createContext, createElement, useContext } from 'react';\nimport type { ReactNode } from 'react';\nimport type { LiveClient } from '@velajs/client';\nimport type { LiveContract } from '@velajs/client';\n\n// Deliberately `any`-typed inside the context: the contract generic is\n// re-applied at the useLiveClient() boundary. One provider serves any app.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst LiveClientContext = createContext<LiveClient<any> | null>(null);\n\nexport interface LiveProviderProps {\n client: LiveClient<LiveContract>;\n children?: ReactNode;\n}\n\n/** Provides the LiveClient to the hook tree. `createElement`-based — no JSX toolchain required. */\nexport function LiveProvider(props: LiveProviderProps): ReturnType<typeof createElement> {\n return createElement(LiveClientContext.Provider, { value: props.client }, props.children);\n}\n\nexport function useLiveClient<C extends LiveContract = LiveContract>(): LiveClient<C> {\n const client = useContext(LiveClientContext);\n if (!client) {\n throw new Error(\n 'useLiveClient: no LiveClient in context — wrap the tree in <LiveProvider client={…}>.',\n );\n }\n return client as LiveClient<C>;\n}\n","import { useCallback, useMemo, useSyncExternalStore } from 'react';\nimport { argsKeyOf } from '@velajs/client';\nimport type { ArgsOf, LiveContract, ResultOf, SubscribeOptions } from '@velajs/client';\nimport { useLiveClient } from './context';\n\nexport interface UseLiveQueryOptions extends SubscribeOptions {\n /** Render without subscribing (conditional queries). */\n skip?: boolean;\n}\n\n/**\n * Subscribe to a live query. Returns `undefined` until the first\n * snapshot/hydrated value, then re-renders on every server push. Built on\n * `useSyncExternalStore` — the client guarantees referentially stable\n * snapshots between notifications, so there is no tearing and no external\n * cache library.\n *\n * ```tsx\n * const todos = useLiveQuery('todos.list', { listId }) ?? [];\n * ```\n */\nexport function useLiveQuery<C extends LiveContract, Q extends keyof C & string>(\n query: Q,\n args: ArgsOf<C, Q>,\n options?: UseLiveQueryOptions,\n): ResultOf<C, Q> | undefined {\n const client = useLiveClient<C>();\n // Stable identity for the deps array — callers pass fresh object literals.\n const argsKey = useMemo(() => argsKeyOf(args), [args]);\n const { room, key, skip, onError } = options ?? {};\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n if (skip) return () => {};\n return client.subscribe(query, args, () => onStoreChange(), { room, key, onError });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps -- argsKey stands in for args; onError is deliberately unbound\n [client, query, argsKey, room, key, skip],\n );\n\n const getSnapshot = useCallback(\n () => (skip ? undefined : client.peek(query, args, room)),\n // eslint-disable-next-line react-hooks/exhaustive-deps -- argsKey stands in for args\n [client, query, argsKey, room, skip],\n );\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n","import { useCallback, useRef, useState } from 'react';\nimport type { MutateOptions } from '@velajs/client';\nimport { useLiveClient } from './context';\n\nexport interface UseLiveMutationResult<R> {\n /** Fire the mutation. Per-call options (optimistic targets, method, …) merge over the hook defaults. */\n mutate: (body?: unknown, options?: MutateOptions) => Promise<R>;\n pending: boolean;\n data: R | undefined;\n error: unknown;\n reset: () => void;\n}\n\n/**\n * HTTP mutation hook with live-aware optimistic updates:\n *\n * ```tsx\n * const { mutate: addTodo, pending } = useLiveMutation('/todos', {\n * optimistic: { query: 'todos.list', args: { listId }, apply: (t = []) => [...t, temp] },\n * });\n * ```\n *\n * The optimistic layer is dropped exactly when a live frame's cursor passes\n * the mutation's `Vela-Commit-Cursor` (see @velajs/client); failures roll it\n * back and reject.\n */\nexport function useLiveMutation<R = unknown>(\n path: string,\n defaults?: MutateOptions,\n): UseLiveMutationResult<R> {\n const client = useLiveClient();\n const [state, setState] = useState<{ pending: number; data?: R; error?: unknown }>({\n pending: 0,\n });\n const defaultsRef = useRef(defaults);\n defaultsRef.current = defaults;\n\n const mutate = useCallback(\n async (body?: unknown, options?: MutateOptions): Promise<R> => {\n setState((current) => ({ ...current, pending: current.pending + 1 }));\n try {\n const data = await client.mutate<R>(path, body, { ...defaultsRef.current, ...options });\n setState((current) => ({ pending: current.pending - 1, data, error: undefined }));\n return data;\n } catch (error) {\n setState((current) => ({ ...current, pending: current.pending - 1, error }));\n throw error;\n }\n },\n [client, path],\n );\n\n return {\n mutate,\n pending: state.pending > 0,\n data: state.data,\n error: state.error,\n reset: useCallback(() => setState({ pending: 0 }), []),\n };\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport type { ConnectionStatus } from '@velajs/client';\nimport { useLiveClient } from './context';\n\n/** The client's aggregate connection status, live. */\nexport function useConnectionStatus(): ConnectionStatus {\n const client = useLiveClient();\n const subscribe = useCallback(\n (onStoreChange: () => void) => client.onConnectionStatus(() => onStoreChange()),\n [client],\n );\n const getSnapshot = useCallback(() => client.connectionStatus(), [client]);\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n","import { useEffect, useRef, useState } from 'react';\nimport { createPresence } from '@velajs/client/presence';\nimport type { PresenceMember } from '@velajs/client/presence';\nimport { useLiveClient } from './context';\n\nexport interface UsePresenceOptions {\n /** Payload attached to this connection's roster entry (latest render's value rides each beat). */\n meta?: unknown;\n /** Keep well under the server TTL (default 10 000 ms vs 30 000 ms). */\n heartbeatIntervalMs?: number;\n}\n\n/**\n * Join a room's presence and observe its roster. Heartbeats ride the room's\n * live socket; departure is immediate on close; a tab regaining visibility\n * beats right away so the roster recovers from a throttled background timer.\n *\n * ```tsx\n * const people = usePresence(roomId, { meta: { name: user.name } });\n * ```\n */\nexport function usePresence(room: string, options?: UsePresenceOptions): PresenceMember[] {\n const client = useLiveClient();\n const [members, setMembers] = useState<PresenceMember[]>([]);\n const metaRef = useRef(options?.meta);\n metaRef.current = options?.meta;\n const heartbeatIntervalMs = options?.heartbeatIntervalMs;\n\n useEffect(() => {\n const handle = createPresence(client, {\n room,\n meta: () => metaRef.current,\n heartbeatIntervalMs,\n onRoster: setMembers,\n });\n\n const doc = (globalThis as { document?: Document }).document;\n const onVisibility = (): void => {\n if (doc?.visibilityState === 'visible') handle.beat();\n };\n doc?.addEventListener?.('visibilitychange', onVisibility);\n\n return () => {\n doc?.removeEventListener?.('visibilitychange', onVisibility);\n handle.stop();\n setMembers([]);\n };\n }, [client, room, heartbeatIntervalMs]);\n\n return members;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,65 +1,65 @@
1
1
  {
2
2
  "name": "@velajs/react",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
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
5
  "keywords": [
22
- "vela",
23
- "react",
24
6
  "hooks",
25
7
  "live-queries",
8
+ "optimistic-updates",
9
+ "react",
26
10
  "realtime",
27
- "optimistic-updates"
11
+ "vela"
28
12
  ],
29
- "author": "ksh",
13
+ "homepage": "https://github.com/velajs/client#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/velajs/client/issues"
16
+ },
30
17
  "license": "MIT",
18
+ "author": "ksh",
31
19
  "repository": {
32
20
  "type": "git",
33
21
  "url": "git+https://github.com/velajs/client.git",
34
22
  "directory": "packages/react"
35
23
  },
36
- "homepage": "https://github.com/velajs/client#readme",
37
- "bugs": {
38
- "url": "https://github.com/velajs/client/issues"
39
- },
40
- "engines": {
41
- "node": ">=20"
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "type": "module",
31
+ "sideEffects": false,
32
+ "main": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ }
42
39
  },
43
40
  "dependencies": {
44
- "@velajs/client": "^0.1.0"
45
- },
46
- "peerDependencies": {
47
- "react": ">=18"
41
+ "@velajs/client": "^0.1.1"
48
42
  },
49
43
  "devDependencies": {
50
- "@swc/cli": "^0.8.1",
51
44
  "@swc/core": "^1.15.43",
52
45
  "@testing-library/react": "^16.3.0",
53
46
  "@types/react": "^19.0.0",
54
47
  "happy-dom": "^20.0.0",
55
48
  "react": "^19.0.0",
56
49
  "react-dom": "^19.0.0",
57
- "typescript": "^6.0.3",
50
+ "tsdown": "^0.22.4",
51
+ "typescript": "^7.0.2",
58
52
  "unplugin-swc": "^1.5.9",
59
- "vitest": "^4.1.9"
53
+ "vitest": "^4.1.10"
54
+ },
55
+ "peerDependencies": {
56
+ "react": ">=18"
57
+ },
58
+ "engines": {
59
+ "node": ">=24"
60
60
  },
61
61
  "scripts": {
62
- "build": "rm -rf dist && swc src -d dist --strip-leading-paths && tsc --emitDeclarationOnly",
62
+ "build": "tsdown",
63
63
  "test": "vitest run",
64
64
  "typecheck": "tsc --noEmit"
65
65
  }
package/dist/context.d.ts DELETED
@@ -1,11 +0,0 @@
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>;
package/dist/context.js DELETED
@@ -1,17 +0,0 @@
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
- }
@@ -1,3 +0,0 @@
1
- import type { ConnectionStatus } from '@velajs/client';
2
- /** The client's aggregate connection status, live. */
3
- export declare function useConnectionStatus(): ConnectionStatus;
@@ -1,12 +0,0 @@
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
- }
@@ -1,23 +0,0 @@
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>;
@@ -1,59 +0,0 @@
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
- }
@@ -1,17 +0,0 @@
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;
@@ -1,46 +0,0 @@
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
- }
@@ -1,17 +0,0 @@
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[];
@@ -1,41 +0,0 @@
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
- }