@stratasync/react 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +91 -0
  2. package/dist/context.d.ts +21 -0
  3. package/dist/context.d.ts.map +1 -0
  4. package/dist/context.js +27 -0
  5. package/dist/context.js.map +1 -0
  6. package/dist/hooks/use-connection-state.d.ts +55 -0
  7. package/dist/hooks/use-connection-state.d.ts.map +1 -0
  8. package/dist/hooks/use-connection-state.js +90 -0
  9. package/dist/hooks/use-connection-state.js.map +1 -0
  10. package/dist/hooks/use-model.d.ts +22 -0
  11. package/dist/hooks/use-model.d.ts.map +1 -0
  12. package/dist/hooks/use-model.js +162 -0
  13. package/dist/hooks/use-model.js.map +1 -0
  14. package/dist/hooks/use-query.d.ts +35 -0
  15. package/dist/hooks/use-query.d.ts.map +1 -0
  16. package/dist/hooks/use-query.js +302 -0
  17. package/dist/hooks/use-query.js.map +1 -0
  18. package/dist/hooks/use-sync-client.d.ts +25 -0
  19. package/dist/hooks/use-sync-client.d.ts.map +1 -0
  20. package/dist/hooks/use-sync-client.js +49 -0
  21. package/dist/hooks/use-sync-client.js.map +1 -0
  22. package/dist/hooks/use-yjs-document.d.ts +108 -0
  23. package/dist/hooks/use-yjs-document.d.ts.map +1 -0
  24. package/dist/hooks/use-yjs-document.js +202 -0
  25. package/dist/hooks/use-yjs-document.js.map +1 -0
  26. package/dist/hooks/use-yjs-presence.d.ts +62 -0
  27. package/dist/hooks/use-yjs-presence.d.ts.map +1 -0
  28. package/dist/hooks/use-yjs-presence.js +230 -0
  29. package/dist/hooks/use-yjs-presence.js.map +1 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +9 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/provider.d.ts +7 -0
  35. package/dist/provider.d.ts.map +1 -0
  36. package/dist/provider.js +117 -0
  37. package/dist/provider.js.map +1 -0
  38. package/dist/types.d.ts +107 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +2 -0
  41. package/dist/types.js.map +1 -0
  42. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @stratasync/react
2
+
3
+ React bindings and hooks for the Done.
4
+
5
+ ## Overview
6
+
7
+ sync-react provides React hooks for data fetching, real-time updates, and collaborative editing:
8
+
9
+ - **SyncProvider** — context provider for the sync client
10
+ - **useModel / useQuery** — data fetching with Suspense or loading states
11
+ - **useConnectionState** — monitor sync connection status
12
+ - **useYjsDocument / useYjsPresence** — collaborative editing and presence
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @stratasync/react
18
+ ```
19
+
20
+ Peer dependencies: `react` ^18.0.0 || ^19.0.0, `yjs` ^13.6.0 (for collaborative features)
21
+
22
+ ## Setup
23
+
24
+ Wrap your app with `SyncProvider`:
25
+
26
+ ```tsx
27
+ import { SyncProvider } from "@stratasync/react";
28
+
29
+ function App() {
30
+ return (
31
+ <SyncProvider config={syncConfig}>
32
+ <YourApp />
33
+ </SyncProvider>
34
+ );
35
+ }
36
+ ```
37
+
38
+ ## Hooks
39
+
40
+ ### Data Fetching
41
+
42
+ ```typescript
43
+ // Single model with Suspense (throws promise while loading)
44
+ const task = useModel("Task", taskId);
45
+
46
+ // Single model with loading state (no Suspense)
47
+ const { data: task, isLoading } = useQuery("Task", {
48
+ where: (i) => i.id === taskId,
49
+ });
50
+
51
+ // Filtered query
52
+ const { data: tasks, isLoading } = useQuery("Task", {
53
+ where: (i) => i.workspaceId === workspaceId && !i.completedAt,
54
+ limit: 50,
55
+ });
56
+
57
+ // Skip query conditionally
58
+ const { data } = useQuery("Task", { skip: !workspaceId });
59
+ ```
60
+
61
+ ### Connection State
62
+
63
+ ```typescript
64
+ const { state, isReady, isSyncing, isOffline } = useConnectionState();
65
+ // state: "idle" | "loading" | "syncing" | "error"
66
+ ```
67
+
68
+ ### Collaborative Editing (Yjs)
69
+
70
+ ```typescript
71
+ const { doc, isConnected, participants, content } = useYjsDocument(
72
+ { entityType: "Task", entityId: taskId, fieldName: "description" },
73
+ { autoConnect: true }
74
+ );
75
+ ```
76
+
77
+ ## Package Structure
78
+
79
+ ```
80
+ src/
81
+ ├── hooks/
82
+ │ ├── use-sync-client.ts — Access SyncClient instance
83
+ │ ├── use-model.ts — Single model (Suspense)
84
+ │ ├── use-query.ts — Filtered queries
85
+ │ ├── use-connection-state.ts — Connection monitoring
86
+ │ ├── use-yjs-document.ts — Collaborative editing
87
+ │ └── use-yjs-presence.ts — Presence tracking
88
+ ├── provider.tsx — SyncProvider component
89
+ ├── context.ts — React context
90
+ └── types.ts — Type definitions
91
+ ```
@@ -0,0 +1,21 @@
1
+ import type { Context } from "react";
2
+ import type { SyncContextValue, SyncStatusContextValue } from "./types.js";
3
+ /**
4
+ * React context for the sync client
5
+ */
6
+ export declare const SyncContext: Context<SyncContextValue | null>;
7
+ /**
8
+ * React context for the sync client instance only.
9
+ * This stays stable across backlog/status updates.
10
+ */
11
+ export declare const SyncClientContext: Context<SyncContextValue["client"] | null>;
12
+ /**
13
+ * React context for sync lifecycle/status state.
14
+ * Backlog is split out to avoid frequent subscription churn.
15
+ */
16
+ export declare const SyncStatusContext: Context<SyncStatusContextValue | null>;
17
+ /**
18
+ * React context for pending backlog count.
19
+ */
20
+ export declare const SyncBacklogContext: Context<number>;
21
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAErC,OAAO,KAAK,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE3E;;GAEG;AACH,eAAO,MAAM,WAAW,kCAA+C,CAAC;AAOxE;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,OAAO,CAAC,gBAAgB,CAAC,QAAQ,CAAC,GAAG,IAAI,CACjB,CAAC;AAGzD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,wCAE7B,CAAC;AAGF;;GAEG;AACH,eAAO,MAAM,kBAAkB,iBAA2B,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { createContext } from "react";
2
+ /**
3
+ * React context for the sync client
4
+ */
5
+ export const SyncContext = createContext(null);
6
+ /**
7
+ * Display name for React DevTools
8
+ */
9
+ SyncContext.displayName = "SyncContext";
10
+ /**
11
+ * React context for the sync client instance only.
12
+ * This stays stable across backlog/status updates.
13
+ */
14
+ export const SyncClientContext = createContext(null);
15
+ SyncClientContext.displayName = "SyncClientContext";
16
+ /**
17
+ * React context for sync lifecycle/status state.
18
+ * Backlog is split out to avoid frequent subscription churn.
19
+ */
20
+ export const SyncStatusContext = createContext(null);
21
+ SyncStatusContext.displayName = "SyncStatusContext";
22
+ /**
23
+ * React context for pending backlog count.
24
+ */
25
+ export const SyncBacklogContext = createContext(0);
26
+ SyncBacklogContext.displayName = "SyncBacklogContext";
27
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAKtC;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,aAAa,CAA0B,IAAI,CAAC,CAAC;AAExE;;GAEG;AACH,WAAW,CAAC,WAAW,GAAG,aAAa,CAAC;AAExC;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAC5B,aAAa,CAAoC,IAAI,CAAC,CAAC;AACzD,iBAAiB,CAAC,WAAW,GAAG,mBAAmB,CAAC;AAEpD;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAC5C,IAAI,CACL,CAAC;AACF,iBAAiB,CAAC,WAAW,GAAG,mBAAmB,CAAC;AAEpD;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,aAAa,CAAS,CAAC,CAAC,CAAC;AAC3D,kBAAkB,CAAC,WAAW,GAAG,oBAAoB,CAAC"}
@@ -0,0 +1,55 @@
1
+ import type { UseConnectionStateResult, UsePendingCountResult } from "../types.js";
2
+ /**
3
+ * Hook to access the current connection state
4
+ *
5
+ * @example
6
+ * ```tsx
7
+ * function ConnectionStatus() {
8
+ * const { status, lastSyncId, backlog, error } = useConnectionState();
9
+ *
10
+ * if (error) return <Badge color="red">Error</Badge>;
11
+ * if (status === 'bootstrapping') return <Badge>Bootstrapping...</Badge>;
12
+ * return <Badge>Sync ID: {lastSyncId} ({backlog} pending)</Badge>;
13
+ * }
14
+ * ```
15
+ */
16
+ export declare const useConnectionState: () => UseConnectionStateResult;
17
+ /**
18
+ * Hook to get the count of pending (unsynced) transactions
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * function PendingIndicator() {
23
+ * const { count, hasPending } = usePendingCount();
24
+ *
25
+ * if (!hasPending) return null;
26
+ * return <Badge>{count} changes pending sync</Badge>;
27
+ * }
28
+ * ```
29
+ */
30
+ export declare const usePendingCount: () => UsePendingCountResult;
31
+ /**
32
+ * Hook to check if the app is offline
33
+ */
34
+ export declare const useIsOffline: () => boolean;
35
+ /**
36
+ * Hook to manually trigger a sync
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * function SyncButton() {
41
+ * const { sync, isSyncing } = useSync();
42
+ *
43
+ * return (
44
+ * <button onClick={sync} disabled={isSyncing}>
45
+ * {isSyncing ? 'Syncing...' : 'Sync Now'}
46
+ * </button>
47
+ * );
48
+ * }
49
+ * ```
50
+ */
51
+ export declare const useSync: () => {
52
+ sync: () => Promise<void>;
53
+ isSyncing: boolean;
54
+ };
55
+ //# sourceMappingURL=use-connection-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-connection-state.d.ts","sourceRoot":"","sources":["../../src/hooks/use-connection-state.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,qBAAqB,EACtB,MAAM,aAAa,CAAC;AAOrB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,QAAO,wBAUrC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,QAAO,qBAOlC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,YAAY,QAAO,OAG/B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,OAAO,QAAO;IACzB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;CAsBpB,CAAC"}
@@ -0,0 +1,90 @@
1
+ import { useCallback, useRef, useState } from "react";
2
+ import { useSyncBacklogValue, useSyncClientInstance, useSyncStatusValue, } from "./use-sync-client.js";
3
+ /**
4
+ * Hook to access the current connection state
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * function ConnectionStatus() {
9
+ * const { status, lastSyncId, backlog, error } = useConnectionState();
10
+ *
11
+ * if (error) return <Badge color="red">Error</Badge>;
12
+ * if (status === 'bootstrapping') return <Badge>Bootstrapping...</Badge>;
13
+ * return <Badge>Sync ID: {lastSyncId} ({backlog} pending)</Badge>;
14
+ * }
15
+ * ```
16
+ */
17
+ export const useConnectionState = () => {
18
+ const { state, lastSyncId, error } = useSyncStatusValue();
19
+ const backlog = useSyncBacklogValue();
20
+ return {
21
+ backlog,
22
+ error,
23
+ lastSyncId,
24
+ status: state,
25
+ };
26
+ };
27
+ /**
28
+ * Hook to get the count of pending (unsynced) transactions
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * function PendingIndicator() {
33
+ * const { count, hasPending } = usePendingCount();
34
+ *
35
+ * if (!hasPending) return null;
36
+ * return <Badge>{count} changes pending sync</Badge>;
37
+ * }
38
+ * ```
39
+ */
40
+ export const usePendingCount = () => {
41
+ const backlog = useSyncBacklogValue();
42
+ return {
43
+ count: backlog,
44
+ hasPending: backlog > 0,
45
+ };
46
+ };
47
+ /**
48
+ * Hook to check if the app is offline
49
+ */
50
+ export const useIsOffline = () => {
51
+ const { isOffline } = useSyncStatusValue();
52
+ return isOffline;
53
+ };
54
+ /**
55
+ * Hook to manually trigger a sync
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * function SyncButton() {
60
+ * const { sync, isSyncing } = useSync();
61
+ *
62
+ * return (
63
+ * <button onClick={sync} disabled={isSyncing}>
64
+ * {isSyncing ? 'Syncing...' : 'Sync Now'}
65
+ * </button>
66
+ * );
67
+ * }
68
+ * ```
69
+ */
70
+ export const useSync = () => {
71
+ const client = useSyncClientInstance();
72
+ const [isSyncing, setIsSyncing] = useState(false);
73
+ const isSyncingRef = useRef(false);
74
+ const sync = useCallback(async () => {
75
+ if (isSyncingRef.current) {
76
+ return;
77
+ }
78
+ isSyncingRef.current = true;
79
+ setIsSyncing(true);
80
+ try {
81
+ await client.syncNow();
82
+ }
83
+ finally {
84
+ isSyncingRef.current = false;
85
+ setIsSyncing(false);
86
+ }
87
+ }, [client]);
88
+ return { isSyncing, sync };
89
+ };
90
+ //# sourceMappingURL=use-connection-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-connection-state.js","sourceRoot":"","sources":["../../src/hooks/use-connection-state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAMtD,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAA6B,EAAE;IAC/D,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,kBAAkB,EAAE,CAAC;IAC1D,MAAM,OAAO,GAAG,mBAAmB,EAAE,CAAC;IAEtC,OAAO;QACL,OAAO;QACP,KAAK;QACL,UAAU;QACV,MAAM,EAAE,KAAK;KACd,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,GAA0B,EAAE;IACzD,MAAM,OAAO,GAAG,mBAAmB,EAAE,CAAC;IAEtC,OAAO;QACL,KAAK,EAAE,OAAO;QACd,UAAU,EAAE,OAAO,GAAG,CAAC;KACxB,CAAC;AACJ,CAAC,CAAC;AAEF;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,GAAY,EAAE;IACxC,MAAM,EAAE,SAAS,EAAE,GAAG,kBAAkB,EAAE,CAAC;IAC3C,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,GAGrB,EAAE;IACF,MAAM,MAAM,GAAG,qBAAqB,EAAE,CAAC;IACvC,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClD,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAEnC,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAClC,IAAI,YAAY,CAAC,OAAO,EAAE,CAAC;YACzB,OAAO;QACT,CAAC;QAED,YAAY,CAAC,OAAO,GAAG,IAAI,CAAC;QAC5B,YAAY,CAAC,IAAI,CAAC,CAAC;QACnB,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;QACzB,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,OAAO,GAAG,KAAK,CAAC;YAC7B,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;IAEb,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;AAC7B,CAAC,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { UseModelResult } from "../types.js";
2
+ /**
3
+ * Hook to access a single model by ID
4
+ * Suspends while bootstrapping or lazy hydration is in progress
5
+ *
6
+ * @param modelName - Name of the model to fetch
7
+ * @param id - ID of the model instance
8
+ * @returns Model instance or null if not found
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * const user = useModel<User>('User', userId);
13
+ * if (!user) return <NotFound />;
14
+ * return <div>{user.name}</div>;
15
+ * ```
16
+ */
17
+ export declare const useModel: <T>(modelName: string, id: string | null | undefined) => T | null;
18
+ /**
19
+ * Non-suspense model hook with loading state
20
+ */
21
+ export declare const useModelState: <T>(modelName: string, id: string | null | undefined) => UseModelResult<T>;
22
+ //# sourceMappingURL=use-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-model.d.ts","sourceRoot":"","sources":["../../src/hooks/use-model.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAOlD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EACxB,WAAW,MAAM,EACjB,IAAI,MAAM,GAAG,IAAI,GAAG,SAAS,KAC5B,CAAC,GAAG,IAmGN,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,CAAC,EAC7B,WAAW,MAAM,EACjB,IAAI,MAAM,GAAG,IAAI,GAAG,SAAS,KAC5B,cAAc,CAAC,CAAC,CAmElB,CAAC"}
@@ -0,0 +1,162 @@
1
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore, } from "react";
2
+ import { useSyncClientInstance, useSyncError, useSyncReady, } from "./use-sync-client.js";
3
+ /**
4
+ * Hook to access a single model by ID
5
+ * Suspends while bootstrapping or lazy hydration is in progress
6
+ *
7
+ * @param modelName - Name of the model to fetch
8
+ * @param id - ID of the model instance
9
+ * @returns Model instance or null if not found
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * const user = useModel<User>('User', userId);
14
+ * if (!user) return <NotFound />;
15
+ * return <div>{user.name}</div>;
16
+ * ```
17
+ */
18
+ export const useModel = (modelName, id) => {
19
+ const client = useSyncClientInstance();
20
+ const isReady = useSyncReady();
21
+ const error = useSyncError();
22
+ const readyPromiseRef = useRef(null);
23
+ const readyPromiseKeyRef = useRef({
24
+ client,
25
+ error,
26
+ id,
27
+ modelName,
28
+ });
29
+ const readyPromiseKey = readyPromiseKeyRef.current;
30
+ if (readyPromiseKey.client !== client ||
31
+ readyPromiseKey.error !== error ||
32
+ readyPromiseKey.id !== id ||
33
+ readyPromiseKey.modelName !== modelName ||
34
+ isReady) {
35
+ readyPromiseRef.current = null;
36
+ readyPromiseKeyRef.current = {
37
+ client,
38
+ error,
39
+ id,
40
+ modelName,
41
+ };
42
+ }
43
+ const subscribe = useCallback((onStoreChange) => {
44
+ if (!id) {
45
+ return () => {
46
+ /* noop */
47
+ };
48
+ }
49
+ return client.onEvent((event) => {
50
+ if (event.type === "modelChange" &&
51
+ event.modelName === modelName &&
52
+ event.modelId === id) {
53
+ onStoreChange();
54
+ }
55
+ });
56
+ }, [client, modelName, id]);
57
+ const getSnapshot = useCallback(() => {
58
+ if (!id) {
59
+ return null;
60
+ }
61
+ return client.getCached(modelName, id);
62
+ }, [client, modelName, id]);
63
+ const model = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
64
+ if (error) {
65
+ throw error;
66
+ }
67
+ if (!id) {
68
+ return null;
69
+ }
70
+ if (!isReady) {
71
+ if (!readyPromiseRef.current) {
72
+ // oxlint-disable-next-line avoid-new -- wrapping callback API in promise
73
+ readyPromiseRef.current = new Promise((resolve, reject) => {
74
+ const unsubscribe = client.onEvent((event) => {
75
+ if (event.type === "syncError") {
76
+ unsubscribe();
77
+ readyPromiseRef.current = null;
78
+ reject(event.error);
79
+ }
80
+ if (event.type === "stateChange" && event.state === "syncing") {
81
+ unsubscribe();
82
+ readyPromiseRef.current = null;
83
+ resolve();
84
+ }
85
+ });
86
+ });
87
+ }
88
+ throw readyPromiseRef.current;
89
+ }
90
+ if (model) {
91
+ return model;
92
+ }
93
+ if (client.isModelMissing(modelName, id)) {
94
+ return null;
95
+ }
96
+ // Suspense requires throwing a promise; discard the model result
97
+ const suspensePromise = client.ensureModel(modelName, id);
98
+ throw suspensePromise;
99
+ };
100
+ /**
101
+ * Non-suspense model hook with loading state
102
+ */
103
+ export const useModelState = (modelName, id) => {
104
+ const client = useSyncClientInstance();
105
+ const isReady = useSyncReady();
106
+ const [data, setData] = useState(null);
107
+ const [isLoading, setIsLoading] = useState(Boolean(id));
108
+ const [error, setError] = useState(null);
109
+ const loadModel = useCallback(async () => {
110
+ if (!id) {
111
+ setData(null);
112
+ setIsLoading(false);
113
+ return;
114
+ }
115
+ if (!isReady) {
116
+ setIsLoading(true);
117
+ return;
118
+ }
119
+ setIsLoading(true);
120
+ setError(null);
121
+ try {
122
+ const result = await client.ensureModel(modelName, id);
123
+ setData(result);
124
+ }
125
+ catch (loadError) {
126
+ setError(loadError instanceof Error ? loadError : new Error(String(loadError)));
127
+ }
128
+ finally {
129
+ setIsLoading(false);
130
+ }
131
+ }, [client, modelName, id, isReady]);
132
+ useEffect(() => {
133
+ // oxlint-disable-next-line prefer-await-to-then -- fire-and-forget pattern
134
+ loadModel().catch(() => {
135
+ /* noop */
136
+ });
137
+ }, [loadModel]);
138
+ useEffect(() => {
139
+ if (!id) {
140
+ return;
141
+ }
142
+ const unsubscribe = client.onEvent((event) => {
143
+ if (event.type === "modelChange" &&
144
+ event.modelName === modelName &&
145
+ event.modelId === id) {
146
+ // oxlint-disable-next-line prefer-await-to-then -- fire-and-forget pattern
147
+ loadModel().catch(() => {
148
+ /* noop */
149
+ });
150
+ }
151
+ });
152
+ return unsubscribe;
153
+ }, [client, modelName, id, loadModel]);
154
+ return {
155
+ data,
156
+ error,
157
+ isFound: data !== null,
158
+ isLoading,
159
+ refresh: loadModel,
160
+ };
161
+ };
162
+ //# sourceMappingURL=use-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-model.js","sourceRoot":"","sources":["../../src/hooks/use-model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import type { UseQueryOptions, UseQueryResult } from "../types.js";
2
+ /**
3
+ * Hook to query models with filtering, sorting, and pagination
4
+ *
5
+ * @param modelName - Name of the model to query
6
+ * @param options - Query options including filters, sorting, and pagination
7
+ * @returns UseQueryResult with data array, loading state, and metadata
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * function TaskList({ projectId }: { projectId: string }) {
12
+ * const { data: tasks, isLoading, hasMore } = useQuery<Task>('Task', {
13
+ * where: (task) => task.projectId === projectId,
14
+ * orderBy: (a, b) => a.createdAt - b.createdAt,
15
+ * limit: 20,
16
+ * });
17
+ *
18
+ * if (isLoading) return <Spinner />;
19
+ *
20
+ * return (
21
+ * <ul>
22
+ * {tasks.map((task) => (
23
+ * <li key={task.id}>{task.title}</li>
24
+ * ))}
25
+ * </ul>
26
+ * );
27
+ * }
28
+ * ```
29
+ */
30
+ export declare const useQuery: <T>(modelName: string, options?: UseQueryOptions<T>) => UseQueryResult<T>;
31
+ /**
32
+ * Hook to query all models of a type
33
+ */
34
+ export declare const useQueryAll: <T>(modelName: string, options?: Omit<UseQueryOptions<T>, "limit" | "offset">) => UseQueryResult<T>;
35
+ //# sourceMappingURL=use-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-query.d.ts","sourceRoot":"","sources":["../../src/hooks/use-query.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAkHnE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,QAAQ,GAAI,CAAC,EACxB,WAAW,MAAM,EACjB,UAAS,eAAe,CAAC,CAAC,CAAM,KAC/B,cAAc,CAAC,CAAC,CA8OlB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,EAC3B,WAAW,MAAM,EACjB,UAAS,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAM,KACzD,cAAc,CAAC,CAAC,CAAoC,CAAC"}