@revealui/sync 0.3.6 → 0.3.7
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 +7 -7
- package/dist/collab/use-collab-document.js +2 -2
- package/dist/conflict-resolution.d.ts +82 -0
- package/dist/conflict-resolution.d.ts.map +1 -0
- package/dist/conflict-resolution.js +166 -0
- package/dist/fetch-with-timeout.d.ts +1 -1
- package/dist/fetch-with-timeout.js +1 -1
- package/dist/hooks/browser-cache-factory.d.ts +15 -0
- package/dist/hooks/browser-cache-factory.d.ts.map +1 -0
- package/dist/hooks/browser-cache-factory.js +16 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useConversations.js +2 -2
- package/dist/hooks/useOfflineCache.d.ts +14 -5
- package/dist/hooks/useOfflineCache.d.ts.map +1 -1
- package/dist/hooks/useOfflineCache.js +171 -70
- package/dist/hooks/useOnlineStatus.js +1 -1
- package/dist/hooks/useShapeCacheInvalidation.d.ts +58 -0
- package/dist/hooks/useShapeCacheInvalidation.d.ts.map +1 -0
- package/dist/hooks/useShapeCacheInvalidation.js +64 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/mutations.js +1 -1
- package/dist/offline-queue.d.ts +1 -1
- package/dist/offline-queue.js +3 -3
- package/dist/provider/index.d.ts +4 -4
- package/dist/provider/index.js +1 -1
- package/package.json +28 -15
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @revealui/sync
|
|
2
2
|
|
|
3
|
-
ElectricSQL sync utilities for RevealUI
|
|
3
|
+
ElectricSQL sync utilities for RevealUI - real-time data synchronization with local-first architecture.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -28,7 +28,7 @@ import { ElectricProvider } from '@revealui/sync/provider'
|
|
|
28
28
|
|
|
29
29
|
export default function App() {
|
|
30
30
|
return (
|
|
31
|
-
<ElectricProvider proxyBaseUrl="https://
|
|
31
|
+
<ElectricProvider proxyBaseUrl="https://admin.revealui.com">
|
|
32
32
|
<YourComponents />
|
|
33
33
|
</ElectricProvider>
|
|
34
34
|
)
|
|
@@ -122,7 +122,7 @@ const {
|
|
|
122
122
|
|
|
123
123
|
### `useConversations(userId)`
|
|
124
124
|
|
|
125
|
-
Subscribe to conversation history. Server-side proxy enforces row-level filtering by session
|
|
125
|
+
Subscribe to conversation history. Server-side proxy enforces row-level filtering by session - the `userId` parameter is for API compatibility but filtering is handled server-side.
|
|
126
126
|
|
|
127
127
|
```typescript
|
|
128
128
|
const {
|
|
@@ -187,13 +187,13 @@ pnpm --filter @revealui/sync typecheck # Type check
|
|
|
187
187
|
- You need real-time data sync between your database and React UI via ElectricSQL
|
|
188
188
|
- You want CRDT-based collaborative editing (Yjs) for multi-user document workflows
|
|
189
189
|
- You need React hooks that subscribe to live database changes with automatic mutation support
|
|
190
|
-
- **Not** for batch data loading or static pages
|
|
191
|
-
- **Not** for offline-first mobile apps
|
|
190
|
+
- **Not** for batch data loading or static pages - use server components with `@revealui/db` directly
|
|
191
|
+
- **Not** for offline-first mobile apps - ElectricSQL targets web clients with persistent connections
|
|
192
192
|
|
|
193
193
|
## JOSHUA Alignment
|
|
194
194
|
|
|
195
|
-
- **Adaptive**: Shape subscriptions dynamically sync only the data your component needs
|
|
196
|
-
- **Sovereign**: Sync runs through your own CMS proxy and PostgreSQL
|
|
195
|
+
- **Adaptive**: Shape subscriptions dynamically sync only the data your component needs - scales from one user to many
|
|
196
|
+
- **Sovereign**: Sync runs through your own CMS proxy and PostgreSQL - no third-party real-time service required
|
|
197
197
|
- **Hermetic**: All mutations go through authenticated REST endpoints; ElectricSQL replication is read-only on the client
|
|
198
198
|
|
|
199
199
|
## License
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useShape } from '@electric-sql/react';
|
|
2
2
|
import { fetchWithTimeout } from '../fetch-with-timeout.js';
|
|
3
3
|
import { useElectricConfig } from '../provider/index.js';
|
|
4
|
-
// UUID v4 pattern
|
|
4
|
+
// UUID v4 pattern - only format accepted as a yjs_documents PK
|
|
5
5
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
6
|
export function useCollabDocument(documentId) {
|
|
7
7
|
const { proxyBaseUrl } = useElectricConfig();
|
|
8
8
|
// Validate before interpolating into the WHERE clause. yjs_documents PKs are
|
|
9
|
-
// always UUIDs
|
|
9
|
+
// always UUIDs - reject anything else so no untrusted string enters the query.
|
|
10
10
|
const isValidId = UUID_RE.test(documentId);
|
|
11
11
|
// Hook must always be called (Rules of Hooks). Pass an impossible WHERE when
|
|
12
12
|
// the ID is invalid so the shape returns no rows but the hook still runs.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict Resolution for Offline Edits
|
|
3
|
+
*
|
|
4
|
+
* Provides version-based conflict detection and configurable resolution
|
|
5
|
+
* strategies for mutations queued while offline.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Each mutation carries a `baseVersion` (the version of the resource
|
|
9
|
+
* at the time the edit was made offline).
|
|
10
|
+
* - On replay, the server may respond with 409 Conflict if the resource
|
|
11
|
+
* has been modified since `baseVersion`.
|
|
12
|
+
* - The resolution strategy determines how to handle the conflict:
|
|
13
|
+
* last-write-wins, server-wins, or manual merge.
|
|
14
|
+
*/
|
|
15
|
+
export type ConflictStrategy = 'last-write-wins' | 'server-wins' | 'manual';
|
|
16
|
+
export interface OfflineMutation {
|
|
17
|
+
/** Unique mutation identifier. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** API endpoint URL. */
|
|
20
|
+
url: string;
|
|
21
|
+
/** HTTP method. */
|
|
22
|
+
method: string;
|
|
23
|
+
/** Request headers (serialized). */
|
|
24
|
+
headers: Record<string, string>;
|
|
25
|
+
/** Request body (serialized JSON string). */
|
|
26
|
+
body: string | null;
|
|
27
|
+
/** Timestamp when the mutation was created offline. */
|
|
28
|
+
timestamp: number;
|
|
29
|
+
/** Version of the resource when the edit was made (for conflict detection). */
|
|
30
|
+
baseVersion?: number;
|
|
31
|
+
/** Resource identifier (e.g. "posts:123") for grouping related mutations. */
|
|
32
|
+
resourceId?: string;
|
|
33
|
+
/** Number of replay attempts. */
|
|
34
|
+
retryCount: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ConflictInfo {
|
|
37
|
+
/** The mutation that caused the conflict. */
|
|
38
|
+
mutation: OfflineMutation;
|
|
39
|
+
/** HTTP status code from the server (typically 409). */
|
|
40
|
+
statusCode: number;
|
|
41
|
+
/** Server's current version of the resource. */
|
|
42
|
+
serverVersion?: number;
|
|
43
|
+
/** Server's current resource data (if provided in the 409 response). */
|
|
44
|
+
serverData?: unknown;
|
|
45
|
+
}
|
|
46
|
+
export interface ReplayResult {
|
|
47
|
+
/** Mutations that were successfully replayed. */
|
|
48
|
+
succeeded: OfflineMutation[];
|
|
49
|
+
/** Mutations that hit a conflict. */
|
|
50
|
+
conflicts: ConflictInfo[];
|
|
51
|
+
/** Mutations that failed for non-conflict reasons (network, 5xx). */
|
|
52
|
+
failed: OfflineMutation[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Apply the configured conflict resolution strategy.
|
|
56
|
+
*
|
|
57
|
+
* - `last-write-wins`: Retry the mutation with a force flag, overwriting the server version.
|
|
58
|
+
* - `server-wins`: Discard the offline mutation, keeping the server state.
|
|
59
|
+
* - `manual`: Return the conflict for UI-level resolution (e.g. a merge dialog).
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolveConflict(conflict: ConflictInfo, strategy: ConflictStrategy): Promise<{
|
|
62
|
+
resolved: boolean;
|
|
63
|
+
retryMutation?: OfflineMutation;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Replay a batch of offline mutations in FIFO order.
|
|
67
|
+
*
|
|
68
|
+
* Mutations targeting the same resource are coalesced: only the most recent
|
|
69
|
+
* mutation per resource is replayed, reducing unnecessary round-trips.
|
|
70
|
+
*
|
|
71
|
+
* @param mutations - Queued mutations in chronological order.
|
|
72
|
+
* @param strategy - Conflict resolution strategy.
|
|
73
|
+
* @returns Replay results grouped by outcome.
|
|
74
|
+
*/
|
|
75
|
+
export declare function replayMutations(mutations: OfflineMutation[], strategy?: ConflictStrategy): Promise<ReplayResult>;
|
|
76
|
+
/**
|
|
77
|
+
* Coalesce mutations targeting the same resource.
|
|
78
|
+
* For each resourceId, keep only the latest mutation.
|
|
79
|
+
* Mutations without a resourceId are always included.
|
|
80
|
+
*/
|
|
81
|
+
export declare function coalesceMutations(mutations: OfflineMutation[]): OfflineMutation[];
|
|
82
|
+
//# sourceMappingURL=conflict-resolution.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conflict-resolution.d.ts","sourceRoot":"","sources":["../src/conflict-resolution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,GAAG,aAAa,GAAG,QAAQ,CAAC;AAE5E,MAAM,WAAW,eAAe;IAC9B,kCAAkC;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,oCAAoC;IACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,6CAA6C;IAC7C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,uDAAuD;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,6CAA6C;IAC7C,QAAQ,EAAE,eAAe,CAAC;IAC1B,wDAAwD;IACxD,UAAU,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,iDAAiD;IACjD,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,qCAAqC;IACrC,SAAS,EAAE,YAAY,EAAE,CAAC;IAC1B,qEAAqE;IACrE,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B;AAID;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,gBAAgB,GACzB,OAAO,CAAC;IAAE,QAAQ,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,eAAe,CAAA;CAAE,CAAC,CAwBjE;AAID;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,eAAe,EAAE,EAC5B,QAAQ,GAAE,gBAAoC,GAC7C,OAAO,CAAC,YAAY,CAAC,CAwFvB;AAID;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAkBjF"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conflict Resolution for Offline Edits
|
|
3
|
+
*
|
|
4
|
+
* Provides version-based conflict detection and configurable resolution
|
|
5
|
+
* strategies for mutations queued while offline.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Each mutation carries a `baseVersion` (the version of the resource
|
|
9
|
+
* at the time the edit was made offline).
|
|
10
|
+
* - On replay, the server may respond with 409 Conflict if the resource
|
|
11
|
+
* has been modified since `baseVersion`.
|
|
12
|
+
* - The resolution strategy determines how to handle the conflict:
|
|
13
|
+
* last-write-wins, server-wins, or manual merge.
|
|
14
|
+
*/
|
|
15
|
+
// ─── Resolution ──────────────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* Apply the configured conflict resolution strategy.
|
|
18
|
+
*
|
|
19
|
+
* - `last-write-wins`: Retry the mutation with a force flag, overwriting the server version.
|
|
20
|
+
* - `server-wins`: Discard the offline mutation, keeping the server state.
|
|
21
|
+
* - `manual`: Return the conflict for UI-level resolution (e.g. a merge dialog).
|
|
22
|
+
*/
|
|
23
|
+
export async function resolveConflict(conflict, strategy) {
|
|
24
|
+
switch (strategy) {
|
|
25
|
+
case 'last-write-wins': {
|
|
26
|
+
// Retry with force header to tell the server to accept regardless of version
|
|
27
|
+
const retryMutation = {
|
|
28
|
+
...conflict.mutation,
|
|
29
|
+
headers: {
|
|
30
|
+
...conflict.mutation.headers,
|
|
31
|
+
'X-Force-Overwrite': 'true',
|
|
32
|
+
'X-Base-Version': String(conflict.serverVersion ?? 0),
|
|
33
|
+
},
|
|
34
|
+
retryCount: conflict.mutation.retryCount + 1,
|
|
35
|
+
};
|
|
36
|
+
return { resolved: true, retryMutation };
|
|
37
|
+
}
|
|
38
|
+
case 'server-wins':
|
|
39
|
+
// Discard the offline mutation
|
|
40
|
+
return { resolved: true };
|
|
41
|
+
case 'manual':
|
|
42
|
+
// Return unresolved for UI handling
|
|
43
|
+
return { resolved: false };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ─── Replay Engine ─────────��─────────────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Replay a batch of offline mutations in FIFO order.
|
|
49
|
+
*
|
|
50
|
+
* Mutations targeting the same resource are coalesced: only the most recent
|
|
51
|
+
* mutation per resource is replayed, reducing unnecessary round-trips.
|
|
52
|
+
*
|
|
53
|
+
* @param mutations - Queued mutations in chronological order.
|
|
54
|
+
* @param strategy - Conflict resolution strategy.
|
|
55
|
+
* @returns Replay results grouped by outcome.
|
|
56
|
+
*/
|
|
57
|
+
export async function replayMutations(mutations, strategy = 'last-write-wins') {
|
|
58
|
+
const result = {
|
|
59
|
+
succeeded: [],
|
|
60
|
+
conflicts: [],
|
|
61
|
+
failed: [],
|
|
62
|
+
};
|
|
63
|
+
// Coalesce: for each resourceId, keep only the latest mutation
|
|
64
|
+
const coalesced = coalesceMutations(mutations);
|
|
65
|
+
for (const mutation of coalesced) {
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(mutation.url, {
|
|
68
|
+
method: mutation.method,
|
|
69
|
+
headers: {
|
|
70
|
+
...mutation.headers,
|
|
71
|
+
...(mutation.baseVersion != null ? { 'If-Match': String(mutation.baseVersion) } : {}),
|
|
72
|
+
},
|
|
73
|
+
body: mutation.body || undefined,
|
|
74
|
+
});
|
|
75
|
+
if (response.ok) {
|
|
76
|
+
result.succeeded.push(mutation);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (response.status === 409) {
|
|
80
|
+
let serverData;
|
|
81
|
+
let serverVersion;
|
|
82
|
+
try {
|
|
83
|
+
const body = await response.json();
|
|
84
|
+
serverData = body.data;
|
|
85
|
+
serverVersion = body.version;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// 409 without JSON body
|
|
89
|
+
}
|
|
90
|
+
const conflict = {
|
|
91
|
+
mutation,
|
|
92
|
+
statusCode: 409,
|
|
93
|
+
serverVersion,
|
|
94
|
+
serverData,
|
|
95
|
+
};
|
|
96
|
+
const resolution = await resolveConflict(conflict, strategy);
|
|
97
|
+
if (resolution.resolved && resolution.retryMutation) {
|
|
98
|
+
// Retry the mutation with force overwrite
|
|
99
|
+
try {
|
|
100
|
+
const retryResponse = await fetch(resolution.retryMutation.url, {
|
|
101
|
+
method: resolution.retryMutation.method,
|
|
102
|
+
headers: resolution.retryMutation.headers,
|
|
103
|
+
body: resolution.retryMutation.body || undefined,
|
|
104
|
+
});
|
|
105
|
+
if (retryResponse.ok) {
|
|
106
|
+
result.succeeded.push(mutation);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
result.failed.push(mutation);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
result.failed.push(mutation);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (resolution.resolved) {
|
|
117
|
+
// server-wins: mutation discarded, count as succeeded (intentionally dropped)
|
|
118
|
+
result.succeeded.push(mutation);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
// manual: surface the conflict
|
|
122
|
+
result.conflicts.push(conflict);
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Server error or other non-conflict failure
|
|
127
|
+
if (response.status >= 500) {
|
|
128
|
+
result.failed.push(mutation);
|
|
129
|
+
// Stop replaying on server errors to avoid cascading failures
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
// Client error (4xx other than 409): discard, not recoverable
|
|
133
|
+
result.failed.push(mutation);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Network error: stop replaying
|
|
137
|
+
result.failed.push(mutation);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
// ─── Coalescing ──────��───────────────────────────────────────────────────────
|
|
144
|
+
/**
|
|
145
|
+
* Coalesce mutations targeting the same resource.
|
|
146
|
+
* For each resourceId, keep only the latest mutation.
|
|
147
|
+
* Mutations without a resourceId are always included.
|
|
148
|
+
*/
|
|
149
|
+
export function coalesceMutations(mutations) {
|
|
150
|
+
const byResource = new Map();
|
|
151
|
+
const ungrouped = [];
|
|
152
|
+
for (const mutation of mutations) {
|
|
153
|
+
if (mutation.resourceId) {
|
|
154
|
+
const existing = byResource.get(mutation.resourceId);
|
|
155
|
+
if (!existing || mutation.timestamp > existing.timestamp) {
|
|
156
|
+
byResource.set(mutation.resourceId, mutation);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
ungrouped.push(mutation);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Maintain chronological order: ungrouped first, then coalesced by timestamp
|
|
164
|
+
const coalesced = [...byResource.values()].sort((a, b) => a.timestamp - b.timestamp);
|
|
165
|
+
return [...ungrouped, ...coalesced];
|
|
166
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A fetch wrapper that aborts requests after {@link SHAPE_FETCH_TIMEOUT_MS} (10 s).
|
|
3
3
|
* Passed as `fetchClient` to ElectricSQL `useShape` so that shape subscription
|
|
4
|
-
* requests to the
|
|
4
|
+
* requests to the admin proxy do not hang indefinitely.
|
|
5
5
|
*/
|
|
6
6
|
export declare function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
7
7
|
//# sourceMappingURL=fetch-with-timeout.d.ts.map
|
|
@@ -3,7 +3,7 @@ const SHAPE_FETCH_TIMEOUT_MS = 10_000;
|
|
|
3
3
|
/**
|
|
4
4
|
* A fetch wrapper that aborts requests after {@link SHAPE_FETCH_TIMEOUT_MS} (10 s).
|
|
5
5
|
* Passed as `fetchClient` to ElectricSQL `useShape` so that shape subscription
|
|
6
|
-
* requests to the
|
|
6
|
+
* requests to the admin proxy do not hang indefinitely.
|
|
7
7
|
*/
|
|
8
8
|
export function fetchWithTimeout(input, init) {
|
|
9
9
|
// If the caller already provides a signal, respect it and compose with our timeout.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser cache factory for PGlite-backed offline cache.
|
|
3
|
+
* Separated into its own file so tests can vi.mock() it without
|
|
4
|
+
* triggering PGlite WASM loading in jsdom.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
interface CacheStoreLike {
|
|
8
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
9
|
+
set<T = unknown>(key: string, value: T, ttlSeconds: number, tags?: string[]): Promise<void>;
|
|
10
|
+
delete(...keys: string[]): Promise<number>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function createOfflineCache(): Promise<CacheStoreLike | null>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=browser-cache-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-cache-factory.d.ts","sourceRoot":"","sources":["../../src/hooks/browser-cache-factory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,UAAU,cAAc;IACtB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACjD,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5F,MAAM,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAUzE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser cache factory for PGlite-backed offline cache.
|
|
3
|
+
* Separated into its own file so tests can vi.mock() it without
|
|
4
|
+
* triggering PGlite WASM loading in jsdom.
|
|
5
|
+
* @internal
|
|
6
|
+
*/
|
|
7
|
+
export async function createOfflineCache() {
|
|
8
|
+
try {
|
|
9
|
+
const specifier = ['@revealui', 'cache', 'adapters'].join('/');
|
|
10
|
+
const mod = await Function('s', 'return import(s)')(specifier);
|
|
11
|
+
return await mod.createBrowserCache({ dbName: 'revealui-offline' });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { useOfflineCache } from './useOfflineCache.js';
|
|
2
2
|
export type { OnlineStatusResult } from './useOnlineStatus.js';
|
|
3
3
|
export { useOnlineStatus } from './useOnlineStatus.js';
|
|
4
|
+
export type { InvalidationAction } from './useShapeCacheInvalidation.js';
|
|
5
|
+
export { useShapeCacheInvalidation } from './useShapeCacheInvalidation.js';
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -4,12 +4,12 @@ import { fetchWithTimeout } from '../fetch-with-timeout.js';
|
|
|
4
4
|
import { useSyncMutations } from '../mutations.js';
|
|
5
5
|
import { useElectricConfig } from '../provider/index.js';
|
|
6
6
|
import { toRecords } from '../shape-utils.js';
|
|
7
|
-
// _userId kept for API compatibility
|
|
7
|
+
// _userId kept for API compatibility - filtering is enforced by the server-side
|
|
8
8
|
// proxy at /api/shapes/conversations, which reads the session cookie directly.
|
|
9
9
|
export function useConversations(_userId) {
|
|
10
10
|
const { proxyBaseUrl } = useElectricConfig();
|
|
11
11
|
// The proxy validates the session and enforces row-level filtering server-side.
|
|
12
|
-
// Client-provided params are not forwarded
|
|
12
|
+
// Client-provided params are not forwarded - the proxy overrides them.
|
|
13
13
|
const { data, isLoading, error } = useShape({
|
|
14
14
|
url: `${proxyBaseUrl}/api/shapes/conversations`,
|
|
15
15
|
fetchClient: fetchWithTimeout,
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
interface UseOfflineCacheOptions {
|
|
2
2
|
/** ElectricSQL shape subscription URL. */
|
|
3
3
|
shapeUrl: string;
|
|
4
|
-
/** Unique key for the
|
|
4
|
+
/** Unique key for the cache entry. */
|
|
5
5
|
cacheKey: string;
|
|
6
6
|
/** How long cached data is considered fresh (seconds). Defaults to 3600. */
|
|
7
7
|
ttlSeconds?: number;
|
|
8
|
+
/** Additional cache tags for targeted invalidation. */
|
|
9
|
+
tags?: string[];
|
|
8
10
|
}
|
|
9
11
|
interface UseOfflineCacheResult<T> {
|
|
10
|
-
/** The current data
|
|
12
|
+
/** The current data: live from the shape when online, cached when offline. */
|
|
11
13
|
data: T[];
|
|
12
14
|
/** Whether the browser has network connectivity. */
|
|
13
15
|
isOnline: boolean;
|
|
@@ -17,13 +19,20 @@ interface UseOfflineCacheResult<T> {
|
|
|
17
19
|
lastSyncedAt: Date | null;
|
|
18
20
|
/** Shape subscription or cache-read error, if any. */
|
|
19
21
|
error: Error | null;
|
|
22
|
+
/** Manually invalidate the cache for this key. */
|
|
23
|
+
invalidate: () => Promise<void>;
|
|
20
24
|
}
|
|
25
|
+
/** Reset singleton state between tests. @internal */
|
|
26
|
+
export declare function _resetCacheState(): void;
|
|
21
27
|
/**
|
|
22
28
|
* Wrap an ElectricSQL `useShape` subscription with offline-first caching.
|
|
23
29
|
*
|
|
24
|
-
* When online the hook delegates to `useShape` and mirrors results into
|
|
25
|
-
*
|
|
26
|
-
* most recent cached snapshot if one exists within the TTL window.
|
|
30
|
+
* When online the hook delegates to `useShape` and mirrors results into a
|
|
31
|
+
* PGlite browser cache (IndexedDB). When offline (or during initial load) it
|
|
32
|
+
* returns the most recent cached snapshot if one exists within the TTL window.
|
|
33
|
+
*
|
|
34
|
+
* Falls back to localStorage when PGlite is unavailable (e.g. private browsing
|
|
35
|
+
* or environments without WASM support).
|
|
27
36
|
*
|
|
28
37
|
* @typeParam T - Row type returned by the shape subscription.
|
|
29
38
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useOfflineCache.d.ts","sourceRoot":"","sources":["../../src/hooks/useOfflineCache.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useOfflineCache.d.ts","sourceRoot":"","sources":["../../src/hooks/useOfflineCache.ts"],"names":[],"mappings":"AAcA,UAAU,sBAAsB;IAC9B,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,UAAU,qBAAqB,CAAC,CAAC;IAC/B,8EAA8E;IAC9E,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,oDAAoD;IACpD,QAAQ,EAAE,OAAO,CAAC;IAClB,sEAAsE;IACtE,SAAS,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,YAAY,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,sDAAsD;IACtD,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,kDAAkD;IAClD,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AAuDD,qDAAqD;AACrD,wBAAgB,gBAAgB,IAAI,IAAI,CAIvC;AAuCD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CAAC,CAAC,CAAC,CAmI5F"}
|
|
@@ -1,112 +1,213 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useShape } from '@electric-sql/react';
|
|
3
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
4
|
import { fetchWithTimeout } from '../fetch-with-timeout.js';
|
|
5
5
|
import { toRecords } from '../shape-utils.js';
|
|
6
6
|
import { useOnlineStatus } from './useOnlineStatus.js';
|
|
7
|
-
/** Prefix for all offline-cache localStorage keys. */
|
|
8
|
-
const CACHE_PREFIX = 'revealui:cache:';
|
|
9
7
|
/** Default time-to-live for cached data (seconds). */
|
|
10
8
|
const DEFAULT_TTL_SECONDS = 3600;
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
9
|
+
/** Tag prefix applied to all offline cache entries for bulk invalidation. */
|
|
10
|
+
const OFFLINE_CACHE_TAG = 'offline-cache';
|
|
11
|
+
let browserCache = null;
|
|
12
|
+
let cacheInitPromise = null;
|
|
13
|
+
let cacheRefCount = 0;
|
|
14
|
+
function isBrowserWithIndexedDB() {
|
|
15
|
+
return (typeof window !== 'undefined' &&
|
|
16
|
+
typeof indexedDB !== 'undefined' &&
|
|
17
|
+
typeof navigator !== 'undefined' &&
|
|
18
|
+
// Exclude jsdom/test environments where PGlite WASM cannot run
|
|
19
|
+
navigator.userAgent.indexOf('jsdom') === -1);
|
|
27
20
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (!
|
|
21
|
+
async function getBrowserCache() {
|
|
22
|
+
if (browserCache)
|
|
23
|
+
return browserCache;
|
|
24
|
+
if (cacheInitPromise)
|
|
25
|
+
return cacheInitPromise;
|
|
26
|
+
if (!isBrowserWithIndexedDB())
|
|
34
27
|
return null;
|
|
28
|
+
cacheInitPromise = (async () => {
|
|
29
|
+
try {
|
|
30
|
+
const { createOfflineCache } = await import('./browser-cache-factory.js');
|
|
31
|
+
const cache = await createOfflineCache();
|
|
32
|
+
browserCache = cache;
|
|
33
|
+
return cache;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
return cacheInitPromise;
|
|
40
|
+
}
|
|
41
|
+
function releaseBrowserCache() {
|
|
42
|
+
cacheRefCount--;
|
|
43
|
+
if (cacheRefCount === 0 && browserCache) {
|
|
44
|
+
browserCache.close().catch(() => {
|
|
45
|
+
/* fire-and-forget cleanup */
|
|
46
|
+
});
|
|
47
|
+
browserCache = null;
|
|
48
|
+
cacheInitPromise = null;
|
|
35
49
|
}
|
|
50
|
+
}
|
|
51
|
+
/** Reset singleton state between tests. @internal */
|
|
52
|
+
export function _resetCacheState() {
|
|
53
|
+
browserCache = null;
|
|
54
|
+
cacheInitPromise = null;
|
|
55
|
+
cacheRefCount = 0;
|
|
56
|
+
}
|
|
57
|
+
const LS_PREFIX = 'revealui:cache:';
|
|
58
|
+
function readLocalStorageCache(key, ttlSeconds) {
|
|
59
|
+
if (typeof window === 'undefined')
|
|
60
|
+
return null;
|
|
36
61
|
try {
|
|
37
|
-
const raw = window.localStorage.getItem(
|
|
38
|
-
if (raw === null)
|
|
62
|
+
const raw = window.localStorage.getItem(LS_PREFIX + key);
|
|
63
|
+
if (raw === null)
|
|
39
64
|
return null;
|
|
40
|
-
}
|
|
41
65
|
const parsed = JSON.parse(raw);
|
|
42
|
-
if (!Array.isArray(parsed.data))
|
|
66
|
+
if (!Array.isArray(parsed.data))
|
|
43
67
|
return null;
|
|
44
|
-
}
|
|
45
|
-
// Check TTL.
|
|
46
68
|
const cachedTime = new Date(parsed.cachedAt).getTime();
|
|
47
|
-
if (Number.isNaN(cachedTime))
|
|
69
|
+
if (Number.isNaN(cachedTime))
|
|
48
70
|
return null;
|
|
49
|
-
|
|
50
|
-
const ageSeconds = (Date.now() - cachedTime) / 1_000;
|
|
51
|
-
if (ageSeconds > ttlSeconds) {
|
|
71
|
+
if ((Date.now() - cachedTime) / 1_000 > ttlSeconds)
|
|
52
72
|
return null;
|
|
53
|
-
|
|
54
|
-
return parsed;
|
|
73
|
+
return parsed.data;
|
|
55
74
|
}
|
|
56
75
|
catch {
|
|
57
76
|
return null;
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*/
|
|
63
|
-
function writeCache(cacheKey, data) {
|
|
64
|
-
if (!hasLocalStorage()) {
|
|
79
|
+
function writeLocalStorageCache(key, data) {
|
|
80
|
+
if (typeof window === 'undefined')
|
|
65
81
|
return;
|
|
66
|
-
}
|
|
67
82
|
try {
|
|
68
|
-
const payload = {
|
|
69
|
-
|
|
70
|
-
cachedAt: new Date().toISOString(),
|
|
71
|
-
};
|
|
72
|
-
window.localStorage.setItem(CACHE_PREFIX + cacheKey, JSON.stringify(payload));
|
|
83
|
+
const payload = { data, cachedAt: new Date().toISOString() };
|
|
84
|
+
window.localStorage.setItem(LS_PREFIX + key, JSON.stringify(payload));
|
|
73
85
|
}
|
|
74
86
|
catch {
|
|
75
|
-
// Quota exceeded or private browsing
|
|
87
|
+
// Quota exceeded or private browsing.
|
|
76
88
|
}
|
|
77
89
|
}
|
|
90
|
+
// ─── Hook ────────────────────────────────────────────────────────────────────
|
|
78
91
|
/**
|
|
79
92
|
* Wrap an ElectricSQL `useShape` subscription with offline-first caching.
|
|
80
93
|
*
|
|
81
|
-
* When online the hook delegates to `useShape` and mirrors results into
|
|
82
|
-
*
|
|
83
|
-
* most recent cached snapshot if one exists within the TTL window.
|
|
94
|
+
* When online the hook delegates to `useShape` and mirrors results into a
|
|
95
|
+
* PGlite browser cache (IndexedDB). When offline (or during initial load) it
|
|
96
|
+
* returns the most recent cached snapshot if one exists within the TTL window.
|
|
97
|
+
*
|
|
98
|
+
* Falls back to localStorage when PGlite is unavailable (e.g. private browsing
|
|
99
|
+
* or environments without WASM support).
|
|
84
100
|
*
|
|
85
101
|
* @typeParam T - Row type returned by the shape subscription.
|
|
86
102
|
*/
|
|
87
103
|
export function useOfflineCache(options) {
|
|
88
|
-
const { shapeUrl, cacheKey, ttlSeconds = DEFAULT_TTL_SECONDS } = options;
|
|
104
|
+
const { shapeUrl, cacheKey, ttlSeconds = DEFAULT_TTL_SECONDS, tags } = options;
|
|
89
105
|
const { isOnline } = useOnlineStatus();
|
|
90
|
-
// Shape subscription — runs continuously; ElectricSQL handles reconnection.
|
|
91
106
|
const shape = useShape({ url: shapeUrl, fetchClient: fetchWithTimeout });
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
// Read localStorage synchronously on first render for instant offline data
|
|
108
|
+
const [cache, setCache] = useState(browserCache);
|
|
109
|
+
const [cachedData, setCachedData] = useState(() => readLocalStorageCache(cacheKey, ttlSeconds));
|
|
110
|
+
const [lastSyncedAt, setLastSyncedAt] = useState(() => {
|
|
111
|
+
// Recover lastSyncedAt from localStorage cache timestamp
|
|
112
|
+
if (typeof window === 'undefined')
|
|
113
|
+
return null;
|
|
114
|
+
try {
|
|
115
|
+
const raw = window.localStorage.getItem(LS_PREFIX + cacheKey);
|
|
116
|
+
if (!raw)
|
|
117
|
+
return null;
|
|
118
|
+
const parsed = JSON.parse(raw);
|
|
119
|
+
const time = new Date(parsed.cachedAt).getTime();
|
|
120
|
+
return Number.isNaN(time) ? null : new Date(parsed.cachedAt);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const mounted = useRef(true);
|
|
94
127
|
const cacheKeyRef = useRef(cacheKey);
|
|
95
128
|
cacheKeyRef.current = cacheKey;
|
|
96
|
-
|
|
129
|
+
const ttlSecondsRef = useRef(ttlSeconds);
|
|
130
|
+
ttlSecondsRef.current = ttlSeconds;
|
|
131
|
+
const tagsRef = useRef(tags);
|
|
132
|
+
tagsRef.current = tags;
|
|
133
|
+
// Initialize PGlite browser cache
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
mounted.current = true;
|
|
136
|
+
cacheRefCount++;
|
|
137
|
+
getBrowserCache()
|
|
138
|
+
.then((c) => {
|
|
139
|
+
if (!mounted.current)
|
|
140
|
+
return null;
|
|
141
|
+
if (c) {
|
|
142
|
+
setCache(c);
|
|
143
|
+
return c.get(cacheKeyRef.current);
|
|
144
|
+
}
|
|
145
|
+
// PGlite unavailable; fall back to localStorage
|
|
146
|
+
const lsData = readLocalStorageCache(cacheKeyRef.current, ttlSecondsRef.current);
|
|
147
|
+
if (lsData && mounted.current)
|
|
148
|
+
setCachedData(lsData);
|
|
149
|
+
return null;
|
|
150
|
+
})
|
|
151
|
+
.then((data) => {
|
|
152
|
+
if (mounted.current && data) {
|
|
153
|
+
setCachedData(data);
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
.catch(() => {
|
|
157
|
+
if (mounted.current) {
|
|
158
|
+
// Fall back to localStorage
|
|
159
|
+
const lsData = readLocalStorageCache(cacheKeyRef.current, ttlSecondsRef.current);
|
|
160
|
+
if (lsData)
|
|
161
|
+
setCachedData(lsData);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return () => {
|
|
165
|
+
mounted.current = false;
|
|
166
|
+
releaseBrowserCache();
|
|
167
|
+
};
|
|
168
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
169
|
+
// Persist live data to PGlite (or localStorage fallback) when shape delivers fresh rows
|
|
97
170
|
const shapeData = shape.data;
|
|
98
171
|
useEffect(() => {
|
|
99
|
-
if (!isOnline)
|
|
172
|
+
if (!isOnline)
|
|
100
173
|
return;
|
|
101
|
-
|
|
102
|
-
if (!Array.isArray(shapeData) || shapeData.length === 0) {
|
|
174
|
+
if (!Array.isArray(shapeData) || shapeData.length === 0)
|
|
103
175
|
return;
|
|
104
|
-
}
|
|
105
176
|
const typed = toRecords(shapeData);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
177
|
+
const allTags = [OFFLINE_CACHE_TAG, ...(tagsRef.current ?? [])];
|
|
178
|
+
if (cache) {
|
|
179
|
+
cache.set(cacheKeyRef.current, typed, ttlSecondsRef.current, allTags).catch(() => {
|
|
180
|
+
// PGlite write failed; fall back to localStorage
|
|
181
|
+
writeLocalStorageCache(cacheKeyRef.current, typed);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
writeLocalStorageCache(cacheKeyRef.current, typed);
|
|
186
|
+
}
|
|
187
|
+
if (mounted.current) {
|
|
188
|
+
setCachedData(typed);
|
|
189
|
+
setLastSyncedAt(new Date());
|
|
190
|
+
}
|
|
191
|
+
}, [shapeData, isOnline, cache]);
|
|
192
|
+
// Manual invalidation
|
|
193
|
+
const invalidate = useCallback(async () => {
|
|
194
|
+
if (cache) {
|
|
195
|
+
await cache.delete(cacheKeyRef.current);
|
|
196
|
+
}
|
|
197
|
+
if (typeof window !== 'undefined') {
|
|
198
|
+
try {
|
|
199
|
+
window.localStorage.removeItem(LS_PREFIX + cacheKeyRef.current);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Ignore
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (mounted.current) {
|
|
206
|
+
setCachedData(null);
|
|
207
|
+
setLastSyncedAt(null);
|
|
208
|
+
}
|
|
209
|
+
}, [cache]);
|
|
210
|
+
// Determine what to return
|
|
110
211
|
if (isOnline && Array.isArray(shapeData) && shapeData.length > 0) {
|
|
111
212
|
return {
|
|
112
213
|
data: toRecords(shapeData),
|
|
@@ -114,16 +215,16 @@ export function useOfflineCache(options) {
|
|
|
114
215
|
isSyncing: shape.isLoading,
|
|
115
216
|
lastSyncedAt,
|
|
116
217
|
error: shape.error || null,
|
|
218
|
+
invalidate,
|
|
117
219
|
};
|
|
118
220
|
}
|
|
119
|
-
// Offline or shape has not loaded yet
|
|
120
|
-
const cached = readCache(cacheKey, ttlSeconds);
|
|
121
|
-
const cachedSyncDate = cached !== null ? new Date(cached.cachedAt) : null;
|
|
221
|
+
// Offline or shape has not loaded yet: use cached data
|
|
122
222
|
return {
|
|
123
|
-
data:
|
|
223
|
+
data: cachedData ?? [],
|
|
124
224
|
isOnline,
|
|
125
225
|
isSyncing: isOnline && shape.isLoading,
|
|
126
|
-
lastSyncedAt
|
|
226
|
+
lastSyncedAt,
|
|
127
227
|
error: shape.error || null,
|
|
228
|
+
invalidate,
|
|
128
229
|
};
|
|
129
230
|
}
|
|
@@ -32,7 +32,7 @@ export function useOnlineStatus() {
|
|
|
32
32
|
const [lastOnlineAt, setLastOnlineAt] = useState(null);
|
|
33
33
|
const resetTimerRef = useRef(null);
|
|
34
34
|
useEffect(() => {
|
|
35
|
-
// No-op during SSR
|
|
35
|
+
// No-op during SSR - the effect only runs in the browser.
|
|
36
36
|
if (!isBrowser()) {
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invalidation action triggered when shape data changes.
|
|
3
|
+
* Maps to CacheInvalidationChannel event types.
|
|
4
|
+
*/
|
|
5
|
+
export type InvalidationAction = {
|
|
6
|
+
type: 'delete';
|
|
7
|
+
keys: string[];
|
|
8
|
+
} | {
|
|
9
|
+
type: 'delete-prefix';
|
|
10
|
+
prefix: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'delete-tags';
|
|
13
|
+
tags: string[];
|
|
14
|
+
} | {
|
|
15
|
+
type: 'clear';
|
|
16
|
+
};
|
|
17
|
+
interface UseShapeCacheInvalidationOptions {
|
|
18
|
+
/** ElectricSQL shape subscription URL to watch. */
|
|
19
|
+
shapeUrl: string;
|
|
20
|
+
/**
|
|
21
|
+
* Determine which cache entries to invalidate when shape data changes.
|
|
22
|
+
* Receives the new data from the shape subscription and returns one or
|
|
23
|
+
* more invalidation actions.
|
|
24
|
+
*/
|
|
25
|
+
getInvalidations: (data: unknown[]) => InvalidationAction[];
|
|
26
|
+
/**
|
|
27
|
+
* Callback to execute the invalidation actions. Typically this calls
|
|
28
|
+
* methods on a CacheStore or CacheInvalidationChannel instance.
|
|
29
|
+
*/
|
|
30
|
+
onInvalidate: (actions: InvalidationAction[]) => Promise<void>;
|
|
31
|
+
/** Whether invalidation is enabled (default: true). */
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to an ElectricSQL shape and trigger cache invalidation
|
|
36
|
+
* when the shape data changes.
|
|
37
|
+
*
|
|
38
|
+
* This hook bridges ElectricSQL's real-time sync with the cache
|
|
39
|
+
* invalidation system, enabling push-based cache busting without polling.
|
|
40
|
+
*
|
|
41
|
+
* Example:
|
|
42
|
+
* useShapeCacheInvalidation({
|
|
43
|
+
* shapeUrl: `${ELECTRIC_URL}/v1/shape?table=posts`,
|
|
44
|
+
* getInvalidations: (data) => [
|
|
45
|
+
* { type: 'delete-tags', tags: ['posts'] },
|
|
46
|
+
* ],
|
|
47
|
+
* onInvalidate: async (actions) => {
|
|
48
|
+
* for (const action of actions) {
|
|
49
|
+
* if (action.type === 'delete-tags') {
|
|
50
|
+
* await channel.publishDeleteTags(action.tags);
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* },
|
|
54
|
+
* });
|
|
55
|
+
*/
|
|
56
|
+
export declare function useShapeCacheInvalidation(options: UseShapeCacheInvalidationOptions): void;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=useShapeCacheInvalidation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useShapeCacheInvalidation.d.ts","sourceRoot":"","sources":["../../src/hooks/useShapeCacheInvalidation.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACzC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtB,UAAU,gCAAgC;IACxC,mDAAmD;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,kBAAkB,EAAE,CAAC;IAC5D;;;OAGG;IACH,YAAY,EAAE,CAAC,OAAO,EAAE,kBAAkB,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,uDAAuD;IACvD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,gCAAgC,GAAG,IAAI,CAyCzF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useShape } from '@electric-sql/react';
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { fetchWithTimeout } from '../fetch-with-timeout.js';
|
|
5
|
+
/**
|
|
6
|
+
* Subscribe to an ElectricSQL shape and trigger cache invalidation
|
|
7
|
+
* when the shape data changes.
|
|
8
|
+
*
|
|
9
|
+
* This hook bridges ElectricSQL's real-time sync with the cache
|
|
10
|
+
* invalidation system, enabling push-based cache busting without polling.
|
|
11
|
+
*
|
|
12
|
+
* Example:
|
|
13
|
+
* useShapeCacheInvalidation({
|
|
14
|
+
* shapeUrl: `${ELECTRIC_URL}/v1/shape?table=posts`,
|
|
15
|
+
* getInvalidations: (data) => [
|
|
16
|
+
* { type: 'delete-tags', tags: ['posts'] },
|
|
17
|
+
* ],
|
|
18
|
+
* onInvalidate: async (actions) => {
|
|
19
|
+
* for (const action of actions) {
|
|
20
|
+
* if (action.type === 'delete-tags') {
|
|
21
|
+
* await channel.publishDeleteTags(action.tags);
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* },
|
|
25
|
+
* });
|
|
26
|
+
*/
|
|
27
|
+
export function useShapeCacheInvalidation(options) {
|
|
28
|
+
const { shapeUrl, getInvalidations, onInvalidate, enabled = true } = options;
|
|
29
|
+
const shape = useShape({ url: shapeUrl, fetchClient: fetchWithTimeout });
|
|
30
|
+
const prevDataRef = useRef(null);
|
|
31
|
+
const mounted = useRef(true);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
mounted.current = true;
|
|
34
|
+
return () => {
|
|
35
|
+
mounted.current = false;
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
const shapeData = shape.data;
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!enabled)
|
|
41
|
+
return;
|
|
42
|
+
if (shape.isLoading)
|
|
43
|
+
return;
|
|
44
|
+
if (!Array.isArray(shapeData))
|
|
45
|
+
return;
|
|
46
|
+
// Skip the initial load (no previous data to compare against)
|
|
47
|
+
if (prevDataRef.current === null) {
|
|
48
|
+
prevDataRef.current = shapeData;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Skip if data reference hasn't changed
|
|
52
|
+
if (prevDataRef.current === shapeData)
|
|
53
|
+
return;
|
|
54
|
+
prevDataRef.current = shapeData;
|
|
55
|
+
const actions = getInvalidations(shapeData);
|
|
56
|
+
if (actions.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
if (mounted.current) {
|
|
59
|
+
onInvalidate(actions).catch(() => {
|
|
60
|
+
// Invalidation is best-effort; stale cache entries will expire via TTL
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}, [shapeData, shape.isLoading, enabled, getInvalidations, onInvalidate]);
|
|
64
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @revealui/sync
|
|
2
|
+
* @revealui/sync - Real-time collaboration and sync primitives.
|
|
3
3
|
*
|
|
4
4
|
* The collab layer (Yjs-based) is fully functional.
|
|
5
5
|
* ElectricProvider provides proxyBaseUrl config to child hooks. All hooks route
|
|
6
|
-
* through the authenticated
|
|
6
|
+
* through the authenticated admin proxy at /api/shapes/* - no direct Electric client.
|
|
7
7
|
*
|
|
8
8
|
* Reads use ElectricSQL shape subscriptions for real-time updates.
|
|
9
|
-
* Writes use REST mutations via /api/sync/*
|
|
9
|
+
* Writes use REST mutations via /api/sync/* - changes propagate to all
|
|
10
10
|
* subscribers automatically through ElectricSQL replication.
|
|
11
11
|
*/
|
|
12
12
|
export type { CollabDocumentState, UseCollaborationOptions, UseCollaborationResult, } from './collab/index.js';
|
|
13
13
|
export { CollabProvider, useCollabDocument, useCollaboration, } from './collab/index.js';
|
|
14
|
+
export type { ConflictInfo, ConflictStrategy, OfflineMutation, ReplayResult, } from './conflict-resolution.js';
|
|
15
|
+
export { coalesceMutations, replayMutations, resolveConflict, } from './conflict-resolution.js';
|
|
14
16
|
export type { AgentContextRecord, CreateAgentContextInput, UpdateAgentContextInput, UseAgentContextsResult, } from './hooks/useAgentContexts.js';
|
|
15
17
|
export { useAgentContexts } from './hooks/useAgentContexts.js';
|
|
16
18
|
export type { AgentMemoryRecord, CreateAgentMemoryInput, UpdateAgentMemoryInput, UseAgentMemoryResult, } from './hooks/useAgentMemory.js';
|
|
@@ -21,6 +23,11 @@ export type { CoordinationSessionRecord, CreateCoordinationSessionInput, UpdateC
|
|
|
21
23
|
export { useCoordinationSessions } from './hooks/useCoordinationSessions.js';
|
|
22
24
|
export type { CoordinationWorkItemRecord, CreateCoordinationWorkItemInput, UpdateCoordinationWorkItemInput, UseCoordinationWorkItemsResult, } from './hooks/useCoordinationWorkItems.js';
|
|
23
25
|
export { useCoordinationWorkItems } from './hooks/useCoordinationWorkItems.js';
|
|
26
|
+
export { useOfflineCache } from './hooks/useOfflineCache.js';
|
|
27
|
+
export type { OnlineStatusResult } from './hooks/useOnlineStatus.js';
|
|
28
|
+
export { useOnlineStatus } from './hooks/useOnlineStatus.js';
|
|
29
|
+
export type { InvalidationAction } from './hooks/useShapeCacheInvalidation.js';
|
|
30
|
+
export { useShapeCacheInvalidation } from './hooks/useShapeCacheInvalidation.js';
|
|
24
31
|
export type { MutationResult } from './mutations.js';
|
|
25
32
|
export { ElectricProvider, useElectricConfig } from './provider/index.js';
|
|
26
33
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,YAAY,EACV,iBAAiB,EACjB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,YAAY,EACV,yBAAyB,EACzB,8BAA8B,EAC9B,8BAA8B,EAC9B,6BAA6B,GAC9B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,uBAAuB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,YAAY,EACV,0BAA0B,EAC1B,+BAA+B,EAC/B,+BAA+B,EAC/B,8BAA8B,GAC/B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,YAAY,EACV,mBAAmB,EACnB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,YAAY,GACb,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,YAAY,EACV,iBAAiB,EACjB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,YAAY,EACV,kBAAkB,EAClB,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,YAAY,EACV,yBAAyB,EACzB,8BAA8B,EAC9B,8BAA8B,EAC9B,6BAA6B,GAC9B,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,uBAAuB,EAAE,MAAM,oCAAoC,CAAC;AAC7E,YAAY,EACV,0BAA0B,EAC1B,+BAA+B,EAC/B,+BAA+B,EAC/B,8BAA8B,GAC/B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,wBAAwB,EAAE,MAAM,qCAAqC,CAAC;AAC/E,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,YAAY,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,YAAY,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAC;AAC/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,sCAAsC,CAAC;AACjF,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @revealui/sync
|
|
2
|
+
* @revealui/sync - Real-time collaboration and sync primitives.
|
|
3
3
|
*
|
|
4
4
|
* The collab layer (Yjs-based) is fully functional.
|
|
5
5
|
* ElectricProvider provides proxyBaseUrl config to child hooks. All hooks route
|
|
6
|
-
* through the authenticated
|
|
6
|
+
* through the authenticated admin proxy at /api/shapes/* - no direct Electric client.
|
|
7
7
|
*
|
|
8
8
|
* Reads use ElectricSQL shape subscriptions for real-time updates.
|
|
9
|
-
* Writes use REST mutations via /api/sync/*
|
|
9
|
+
* Writes use REST mutations via /api/sync/* - changes propagate to all
|
|
10
10
|
* subscribers automatically through ElectricSQL replication.
|
|
11
11
|
*/
|
|
12
12
|
export { CollabProvider, useCollabDocument, useCollaboration, } from './collab/index.js';
|
|
13
|
+
export { coalesceMutations, replayMutations, resolveConflict, } from './conflict-resolution.js';
|
|
13
14
|
export { useAgentContexts } from './hooks/useAgentContexts.js';
|
|
14
15
|
export { useAgentMemory } from './hooks/useAgentMemory.js';
|
|
15
16
|
export { useConversations } from './hooks/useConversations.js';
|
|
16
17
|
export { useCoordinationSessions } from './hooks/useCoordinationSessions.js';
|
|
17
18
|
export { useCoordinationWorkItems } from './hooks/useCoordinationWorkItems.js';
|
|
19
|
+
export { useOfflineCache } from './hooks/useOfflineCache.js';
|
|
20
|
+
export { useOnlineStatus } from './hooks/useOnlineStatus.js';
|
|
21
|
+
export { useShapeCacheInvalidation } from './hooks/useShapeCacheInvalidation.js';
|
|
18
22
|
export { ElectricProvider, useElectricConfig } from './provider/index.js';
|
package/dist/mutations.js
CHANGED
|
@@ -4,7 +4,7 @@ import { useElectricConfig } from './provider/index.js';
|
|
|
4
4
|
/** Default timeout for mutation fetch requests (milliseconds). */
|
|
5
5
|
const MUTATION_FETCH_TIMEOUT_MS = 10_000;
|
|
6
6
|
/**
|
|
7
|
-
* Make an authenticated mutation request to the
|
|
7
|
+
* Make an authenticated mutation request to the admin API.
|
|
8
8
|
* Credentials are included so the session cookie is sent cross-origin.
|
|
9
9
|
* Requests are aborted after {@link MUTATION_FETCH_TIMEOUT_MS} (10 s).
|
|
10
10
|
*/
|
package/dist/offline-queue.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Offline mutation queue
|
|
2
|
+
* Offline mutation queue - stores pending mutations in localStorage
|
|
3
3
|
* so they survive page reloads and can be flushed when connectivity returns.
|
|
4
4
|
*/
|
|
5
5
|
/** A single pending mutation waiting to be sent to the server. */
|
package/dist/offline-queue.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Offline mutation queue
|
|
2
|
+
* Offline mutation queue - stores pending mutations in localStorage
|
|
3
3
|
* so they survive page reloads and can be flushed when connectivity returns.
|
|
4
4
|
*/
|
|
5
5
|
/** localStorage key for the persisted queue. */
|
|
@@ -57,7 +57,7 @@ function writeQueue(queue) {
|
|
|
57
57
|
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(queue));
|
|
58
58
|
}
|
|
59
59
|
catch {
|
|
60
|
-
// Quota exceeded or private browsing
|
|
60
|
+
// Quota exceeded or private browsing - drop silently.
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
@@ -120,7 +120,7 @@ export class OfflineMutationQueue {
|
|
|
120
120
|
window.localStorage.removeItem(STORAGE_KEY);
|
|
121
121
|
}
|
|
122
122
|
catch {
|
|
123
|
-
// Ignore
|
|
123
|
+
// Ignore - same guard as writeQueue.
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
}
|
package/dist/provider/index.d.ts
CHANGED
|
@@ -2,14 +2,14 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
interface ElectricContextValue {
|
|
3
3
|
/**
|
|
4
4
|
* Direct Electric service URL (e.g. the Railway instance).
|
|
5
|
-
* Stored in context for future use
|
|
5
|
+
* Stored in context for future use - not consumed by the current proxy-based hooks.
|
|
6
6
|
* All hooks use proxyBaseUrl + /api/shapes/* instead.
|
|
7
7
|
*/
|
|
8
8
|
serviceUrl: string | null;
|
|
9
9
|
/**
|
|
10
|
-
* Base URL prefix for authenticated
|
|
10
|
+
* Base URL prefix for authenticated admin shape proxy routes.
|
|
11
11
|
* Default '' keeps all hook URLs relative (works for same-origin apps).
|
|
12
|
-
* Set to 'https://
|
|
12
|
+
* Set to 'https://admin.revealui.com' when consuming from a different origin.
|
|
13
13
|
*/
|
|
14
14
|
proxyBaseUrl: string;
|
|
15
15
|
debug: boolean;
|
|
@@ -18,7 +18,7 @@ interface ElectricContextValue {
|
|
|
18
18
|
* Provides ElectricSQL configuration to child hooks (`useConversations`, `useCollabDocument`).
|
|
19
19
|
*
|
|
20
20
|
* Provides proxyBaseUrl (and optional serviceUrl/debug) to child hooks via context.
|
|
21
|
-
* All hooks use the
|
|
21
|
+
* All hooks use the admin proxy pattern - no direct Electric connection is established here.
|
|
22
22
|
*/
|
|
23
23
|
export declare function ElectricProvider(props: {
|
|
24
24
|
children: ReactNode;
|
package/dist/provider/index.js
CHANGED
|
@@ -10,7 +10,7 @@ const ElectricContext = createContext({
|
|
|
10
10
|
* Provides ElectricSQL configuration to child hooks (`useConversations`, `useCollabDocument`).
|
|
11
11
|
*
|
|
12
12
|
* Provides proxyBaseUrl (and optional serviceUrl/debug) to child hooks via context.
|
|
13
|
-
* All hooks use the
|
|
13
|
+
* All hooks use the admin proxy pattern - no direct Electric connection is established here.
|
|
14
14
|
*/
|
|
15
15
|
export function ElectricProvider(props) {
|
|
16
16
|
const value = useMemo(() => ({
|
package/package.json
CHANGED
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@revealui/sync",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "ElectricSQL sync utilities for RevealUI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@electric-sql/react": "^1.0.
|
|
7
|
+
"@electric-sql/react": "^1.0.43",
|
|
8
8
|
"lib0": "^0.2.117",
|
|
9
|
-
"ws": "^8.
|
|
9
|
+
"ws": "^8.20.0",
|
|
10
10
|
"y-protocols": "^1.0.7",
|
|
11
|
-
"yjs": "^13.6.
|
|
12
|
-
"@revealui/
|
|
13
|
-
"@revealui/
|
|
11
|
+
"yjs": "^13.6.30",
|
|
12
|
+
"@revealui/cache": "0.1.4",
|
|
13
|
+
"@revealui/contracts": "1.3.7",
|
|
14
|
+
"@revealui/db": "0.3.7"
|
|
14
15
|
},
|
|
15
16
|
"devDependencies": {
|
|
16
|
-
"@testing-library/jest-dom": "^6.
|
|
17
|
-
"@testing-library/react": "^16.3.
|
|
18
|
-
"@types/ws": "^8.
|
|
19
|
-
"@vitest/coverage-v8": "^4.1.
|
|
20
|
-
"jsdom": "
|
|
21
|
-
"react": "^19.2.
|
|
22
|
-
"react-dom": "^19.2.
|
|
17
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
18
|
+
"@testing-library/react": "^16.3.2",
|
|
19
|
+
"@types/ws": "^8.18.1",
|
|
20
|
+
"@vitest/coverage-v8": "^4.1.3",
|
|
21
|
+
"jsdom": "29.0.1",
|
|
22
|
+
"react": "^19.2.5",
|
|
23
|
+
"react-dom": "^19.2.5",
|
|
23
24
|
"typescript": "^6.0.2",
|
|
24
|
-
"vitest": "^4.1.
|
|
25
|
-
"dev": "0.0
|
|
25
|
+
"vitest": "^4.1.3",
|
|
26
|
+
"@revealui/dev": "0.1.0"
|
|
26
27
|
},
|
|
27
28
|
"engines": {
|
|
28
29
|
"node": ">=24.13.0"
|
|
@@ -61,6 +62,18 @@
|
|
|
61
62
|
"url": "https://github.com/RevealUIStudio/revealui.git",
|
|
62
63
|
"directory": "packages/sync"
|
|
63
64
|
},
|
|
65
|
+
"homepage": "https://revealui.com",
|
|
66
|
+
"author": "RevealUI Studio <founder@revealui.com>",
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/RevealUIStudio/revealui/issues"
|
|
69
|
+
},
|
|
70
|
+
"keywords": [
|
|
71
|
+
"revealui",
|
|
72
|
+
"sync",
|
|
73
|
+
"real-time",
|
|
74
|
+
"electricsql",
|
|
75
|
+
"collaborative"
|
|
76
|
+
],
|
|
64
77
|
"scripts": {
|
|
65
78
|
"build": "tsc",
|
|
66
79
|
"clean": "rm -rf dist",
|