@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 +9 -0
- package/dist/index.d.ts +78 -9
- package/dist/index.js +161 -5
- package/dist/index.js.map +1 -0
- package/package.json +35 -35
- package/dist/context.d.ts +0 -11
- package/dist/context.js +0 -17
- package/dist/use-connection-status.d.ts +0 -3
- package/dist/use-connection-status.js +0 -12
- package/dist/use-live-mutation.d.ts +0 -23
- package/dist/use-live-mutation.js +0 -59
- package/dist/use-live-query.d.ts +0 -17
- package/dist/use-live-query.js +0 -46
- package/dist/use-presence.d.ts +0 -17
- package/dist/use-presence.js +0 -41
package/CHANGELOG.md
ADDED
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,78 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
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
|
-
"
|
|
11
|
+
"vela"
|
|
28
12
|
],
|
|
29
|
-
"
|
|
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
|
-
"
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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.
|
|
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
|
-
"
|
|
50
|
+
"tsdown": "^0.22.4",
|
|
51
|
+
"typescript": "^7.0.2",
|
|
58
52
|
"unplugin-swc": "^1.5.9",
|
|
59
|
-
"vitest": "^4.1.
|
|
53
|
+
"vitest": "^4.1.10"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"react": ">=18"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=24"
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
|
-
"build": "
|
|
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,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
|
-
}
|
package/dist/use-live-query.d.ts
DELETED
|
@@ -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;
|
package/dist/use-live-query.js
DELETED
|
@@ -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
|
-
}
|
package/dist/use-presence.d.ts
DELETED
|
@@ -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[];
|
package/dist/use-presence.js
DELETED
|
@@ -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
|
-
}
|