@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.
- package/README.md +91 -0
- package/dist/context.d.ts +21 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +27 -0
- package/dist/context.js.map +1 -0
- package/dist/hooks/use-connection-state.d.ts +55 -0
- package/dist/hooks/use-connection-state.d.ts.map +1 -0
- package/dist/hooks/use-connection-state.js +90 -0
- package/dist/hooks/use-connection-state.js.map +1 -0
- package/dist/hooks/use-model.d.ts +22 -0
- package/dist/hooks/use-model.d.ts.map +1 -0
- package/dist/hooks/use-model.js +162 -0
- package/dist/hooks/use-model.js.map +1 -0
- package/dist/hooks/use-query.d.ts +35 -0
- package/dist/hooks/use-query.d.ts.map +1 -0
- package/dist/hooks/use-query.js +302 -0
- package/dist/hooks/use-query.js.map +1 -0
- package/dist/hooks/use-sync-client.d.ts +25 -0
- package/dist/hooks/use-sync-client.d.ts.map +1 -0
- package/dist/hooks/use-sync-client.js +49 -0
- package/dist/hooks/use-sync-client.js.map +1 -0
- package/dist/hooks/use-yjs-document.d.ts +108 -0
- package/dist/hooks/use-yjs-document.d.ts.map +1 -0
- package/dist/hooks/use-yjs-document.js +202 -0
- package/dist/hooks/use-yjs-document.js.map +1 -0
- package/dist/hooks/use-yjs-presence.d.ts +62 -0
- package/dist/hooks/use-yjs-presence.d.ts.map +1 -0
- package/dist/hooks/use-yjs-presence.js +230 -0
- package/dist/hooks/use-yjs-presence.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/provider.d.ts +7 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +117 -0
- package/dist/provider.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- 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"}
|
package/dist/context.js
ADDED
|
@@ -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"}
|