@syncular/react 0.15.4 → 0.15.5
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 +15 -3
- package/dist/provider.d.ts +1 -1
- package/dist/provider.js +2 -2
- package/dist/resource.d.ts +2 -0
- package/dist/resource.js +42 -28
- package/package.json +4 -4
- package/src/provider.ts +6 -3
- package/src/resource.ts +49 -31
package/README.md
CHANGED
|
@@ -67,8 +67,9 @@ A ready client can be passed directly:
|
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
For async engines, create one resource outside React render. It owns one
|
|
70
|
-
initialization attempt across StrictMode remounts
|
|
71
|
-
once when
|
|
70
|
+
initialization attempt across StrictMode remounts, can retry a failed startup
|
|
71
|
+
without replacing the provider, and closes the client exactly once when
|
|
72
|
+
explicitly disposed:
|
|
72
73
|
|
|
73
74
|
```tsx
|
|
74
75
|
import { createSyncClientResource, SyncProvider } from '@syncular/react';
|
|
@@ -78,7 +79,12 @@ const clientResource = createSyncClientResource(() => createClient());
|
|
|
78
79
|
<SyncProvider
|
|
79
80
|
client={clientResource}
|
|
80
81
|
fallback={<p>Starting local database…</p>}
|
|
81
|
-
renderError={(error) =>
|
|
82
|
+
renderError={(error, retry) => (
|
|
83
|
+
<div>
|
|
84
|
+
<p>{error.message}</p>
|
|
85
|
+
<button onClick={() => void retry()}>Try again</button>
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
82
88
|
>
|
|
83
89
|
<App />
|
|
84
90
|
</SyncProvider>
|
|
@@ -87,6 +93,12 @@ const clientResource = createSyncClientResource(() => createClient());
|
|
|
87
93
|
Call `await clientResource.dispose()` from the application's real lifecycle
|
|
88
94
|
owner, not from a StrictMode-sensitive child effect.
|
|
89
95
|
|
|
96
|
+
The resource is stable across React remounts, not JavaScript module replacement.
|
|
97
|
+
During development, preserve it in the bundler's hot-module data or dispose the
|
|
98
|
+
old resource before creating another one. Otherwise the old worker and the new
|
|
99
|
+
worker can briefly compete for the same persistent OPFS directory. Never wipe
|
|
100
|
+
or rename the database in response to a retryable startup error.
|
|
101
|
+
|
|
90
102
|
## `useMutation`
|
|
91
103
|
|
|
92
104
|
`useMutation()` retains the raw batch API. `useMutation(generatedTable)` adds
|
package/dist/provider.d.ts
CHANGED
|
@@ -16,6 +16,6 @@ export interface SyncProviderProps {
|
|
|
16
16
|
readonly client: SyncClientLike | SyncClientResource;
|
|
17
17
|
readonly children?: ReactNode;
|
|
18
18
|
readonly fallback?: ReactNode;
|
|
19
|
-
readonly renderError?: (error: Error) => ReactNode;
|
|
19
|
+
readonly renderError?: (error: Error, retry: () => Promise<void>) => ReactNode;
|
|
20
20
|
}
|
|
21
21
|
export declare function SyncProvider(props: SyncProviderProps): ReactNode;
|
package/dist/provider.js
CHANGED
|
@@ -56,8 +56,8 @@ export function SyncProvider(props) {
|
|
|
56
56
|
if (snapshot.phase === 'pending')
|
|
57
57
|
return props.fallback ?? null;
|
|
58
58
|
if (snapshot.phase === 'error') {
|
|
59
|
-
if (props.renderError !== undefined)
|
|
60
|
-
return props.renderError(snapshot.error);
|
|
59
|
+
if (props.renderError !== undefined && resource !== undefined)
|
|
60
|
+
return props.renderError(snapshot.error, resource.retry);
|
|
61
61
|
throw snapshot.error;
|
|
62
62
|
}
|
|
63
63
|
return createElement(ReadySyncProvider, { client: snapshot.client }, props.children);
|
package/dist/resource.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface SyncClientResource {
|
|
|
12
12
|
readonly kind: 'syncular-client-resource';
|
|
13
13
|
subscribe(listener: () => void): () => void;
|
|
14
14
|
getSnapshot(): SyncClientResourceSnapshot;
|
|
15
|
+
/** Re-run a failed initialization attempt. Pending/ready calls are no-ops. */
|
|
16
|
+
retry(): Promise<void>;
|
|
15
17
|
dispose(): Promise<void>;
|
|
16
18
|
}
|
|
17
19
|
export declare function createSyncClientResource(factory: () => SyncClientLike | Promise<SyncClientLike>): SyncClientResource;
|
package/dist/resource.js
CHANGED
|
@@ -3,31 +3,43 @@ export function createSyncClientResource(factory) {
|
|
|
3
3
|
let snapshot = { phase: 'pending' };
|
|
4
4
|
let disposed = false;
|
|
5
5
|
let closed = false;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.then(async (client) => {
|
|
9
|
-
if (disposed) {
|
|
10
|
-
const close = client
|
|
11
|
-
.close;
|
|
12
|
-
if (close !== undefined && !closed) {
|
|
13
|
-
closed = true;
|
|
14
|
-
await close.call(client);
|
|
15
|
-
}
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
snapshot = { phase: 'ready', client };
|
|
19
|
-
for (const listener of listeners)
|
|
20
|
-
listener();
|
|
21
|
-
}, (error) => {
|
|
22
|
-
if (disposed)
|
|
23
|
-
return;
|
|
24
|
-
snapshot = {
|
|
25
|
-
phase: 'error',
|
|
26
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
27
|
-
};
|
|
6
|
+
let initialized;
|
|
7
|
+
const notify = () => {
|
|
28
8
|
for (const listener of listeners)
|
|
29
9
|
listener();
|
|
30
|
-
}
|
|
10
|
+
};
|
|
11
|
+
const closeClient = async (client) => {
|
|
12
|
+
const close = client.close;
|
|
13
|
+
if (close !== undefined && !closed) {
|
|
14
|
+
closed = true;
|
|
15
|
+
await close.call(client);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const initialize = (publishPending) => {
|
|
19
|
+
if (publishPending) {
|
|
20
|
+
snapshot = { phase: 'pending' };
|
|
21
|
+
notify();
|
|
22
|
+
}
|
|
23
|
+
return Promise.resolve()
|
|
24
|
+
.then(factory)
|
|
25
|
+
.then(async (client) => {
|
|
26
|
+
if (disposed) {
|
|
27
|
+
await closeClient(client);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
snapshot = { phase: 'ready', client };
|
|
31
|
+
notify();
|
|
32
|
+
}, (error) => {
|
|
33
|
+
if (disposed)
|
|
34
|
+
return;
|
|
35
|
+
snapshot = {
|
|
36
|
+
phase: 'error',
|
|
37
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
38
|
+
};
|
|
39
|
+
notify();
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
initialized = initialize(false);
|
|
31
43
|
return {
|
|
32
44
|
kind: 'syncular-client-resource',
|
|
33
45
|
subscribe(listener) {
|
|
@@ -35,17 +47,19 @@ export function createSyncClientResource(factory) {
|
|
|
35
47
|
return () => listeners.delete(listener);
|
|
36
48
|
},
|
|
37
49
|
getSnapshot: () => snapshot,
|
|
50
|
+
retry() {
|
|
51
|
+
if (disposed || snapshot.phase !== 'error')
|
|
52
|
+
return initialized;
|
|
53
|
+
initialized = initialize(true);
|
|
54
|
+
return initialized;
|
|
55
|
+
},
|
|
38
56
|
async dispose() {
|
|
39
57
|
if (disposed)
|
|
40
58
|
return;
|
|
41
59
|
disposed = true;
|
|
42
60
|
await initialized;
|
|
43
61
|
if (snapshot.phase === 'ready' && !closed) {
|
|
44
|
-
|
|
45
|
-
if (close !== undefined) {
|
|
46
|
-
closed = true;
|
|
47
|
-
await close.call(snapshot.client);
|
|
48
|
-
}
|
|
62
|
+
await closeClient(snapshot.client);
|
|
49
63
|
}
|
|
50
64
|
listeners.clear();
|
|
51
65
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/react",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
4
4
|
"description": "React hooks for Syncular offline-first sync",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
"test": "bun test --preload ./test/setup.ts"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@syncular/client": "0.15.
|
|
51
|
+
"@syncular/client": "0.15.5"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"react": ">=18.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@happy-dom/global-registrator": "^15.11.0",
|
|
58
|
-
"@syncular/core": "0.15.
|
|
59
|
-
"@syncular/server": "0.15.
|
|
58
|
+
"@syncular/core": "0.15.5",
|
|
59
|
+
"@syncular/server": "0.15.5",
|
|
60
60
|
"@testing-library/react": "^16.1.0",
|
|
61
61
|
"@types/react": "^18.3.0",
|
|
62
62
|
"react": "^18.3.1",
|
package/src/provider.ts
CHANGED
|
@@ -33,7 +33,10 @@ export interface SyncProviderProps {
|
|
|
33
33
|
readonly client: SyncClientLike | SyncClientResource;
|
|
34
34
|
readonly children?: ReactNode;
|
|
35
35
|
readonly fallback?: ReactNode;
|
|
36
|
-
readonly renderError?: (
|
|
36
|
+
readonly renderError?: (
|
|
37
|
+
error: Error,
|
|
38
|
+
retry: () => Promise<void>,
|
|
39
|
+
) => ReactNode;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
interface ClientRecord {
|
|
@@ -106,8 +109,8 @@ export function SyncProvider(props: SyncProviderProps): ReactNode {
|
|
|
106
109
|
);
|
|
107
110
|
if (snapshot.phase === 'pending') return props.fallback ?? null;
|
|
108
111
|
if (snapshot.phase === 'error') {
|
|
109
|
-
if (props.renderError !== undefined)
|
|
110
|
-
return props.renderError(snapshot.error);
|
|
112
|
+
if (props.renderError !== undefined && resource !== undefined)
|
|
113
|
+
return props.renderError(snapshot.error, resource.retry);
|
|
111
114
|
throw snapshot.error;
|
|
112
115
|
}
|
|
113
116
|
return createElement(
|
package/src/resource.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface SyncClientResource {
|
|
|
9
9
|
readonly kind: 'syncular-client-resource';
|
|
10
10
|
subscribe(listener: () => void): () => void;
|
|
11
11
|
getSnapshot(): SyncClientResourceSnapshot;
|
|
12
|
+
/** Re-run a failed initialization attempt. Pending/ready calls are no-ops. */
|
|
13
|
+
retry(): Promise<void>;
|
|
12
14
|
dispose(): Promise<void>;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -19,31 +21,48 @@ export function createSyncClientResource(
|
|
|
19
21
|
let snapshot: SyncClientResourceSnapshot = { phase: 'pending' };
|
|
20
22
|
let disposed = false;
|
|
21
23
|
let closed = false;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
let initialized: Promise<void>;
|
|
25
|
+
|
|
26
|
+
const notify = (): void => {
|
|
27
|
+
for (const listener of listeners) listener();
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const closeClient = async (client: SyncClientLike): Promise<void> => {
|
|
31
|
+
const close = (client as { close?: () => void | Promise<void> }).close;
|
|
32
|
+
if (close !== undefined && !closed) {
|
|
33
|
+
closed = true;
|
|
34
|
+
await close.call(client);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const initialize = (publishPending: boolean): Promise<void> => {
|
|
39
|
+
if (publishPending) {
|
|
40
|
+
snapshot = { phase: 'pending' };
|
|
41
|
+
notify();
|
|
42
|
+
}
|
|
43
|
+
return Promise.resolve()
|
|
44
|
+
.then(factory)
|
|
45
|
+
.then(
|
|
46
|
+
async (client) => {
|
|
47
|
+
if (disposed) {
|
|
48
|
+
await closeClient(client);
|
|
49
|
+
return;
|
|
32
50
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
snapshot = { phase: 'ready', client };
|
|
52
|
+
notify();
|
|
53
|
+
},
|
|
54
|
+
(error: unknown) => {
|
|
55
|
+
if (disposed) return;
|
|
56
|
+
snapshot = {
|
|
57
|
+
phase: 'error',
|
|
58
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
59
|
+
};
|
|
60
|
+
notify();
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
initialized = initialize(false);
|
|
47
66
|
|
|
48
67
|
return {
|
|
49
68
|
kind: 'syncular-client-resource',
|
|
@@ -52,18 +71,17 @@ export function createSyncClientResource(
|
|
|
52
71
|
return () => listeners.delete(listener);
|
|
53
72
|
},
|
|
54
73
|
getSnapshot: () => snapshot,
|
|
74
|
+
retry() {
|
|
75
|
+
if (disposed || snapshot.phase !== 'error') return initialized;
|
|
76
|
+
initialized = initialize(true);
|
|
77
|
+
return initialized;
|
|
78
|
+
},
|
|
55
79
|
async dispose() {
|
|
56
80
|
if (disposed) return;
|
|
57
81
|
disposed = true;
|
|
58
82
|
await initialized;
|
|
59
83
|
if (snapshot.phase === 'ready' && !closed) {
|
|
60
|
-
|
|
61
|
-
snapshot.client as { close?: () => void | Promise<void> }
|
|
62
|
-
).close;
|
|
63
|
-
if (close !== undefined) {
|
|
64
|
-
closed = true;
|
|
65
|
-
await close.call(snapshot.client);
|
|
66
|
-
}
|
|
84
|
+
await closeClient(snapshot.client);
|
|
67
85
|
}
|
|
68
86
|
listeners.clear();
|
|
69
87
|
},
|