@syncular/react 0.15.40 → 0.15.43

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 CHANGED
@@ -153,14 +153,18 @@ old resource before creating another one. Otherwise the old worker and the new
153
153
  worker can briefly compete for the same persistent OPFS directory. Never wipe
154
154
  or rename the database in response to a retryable startup error.
155
155
 
156
- For Vite, use `retainViteSyncClientResource(hot.data, schema.version,
157
- createClient)`. It retains a `{ schemaVersion, runtimeVersion, resource }`
158
- record, reuses it for same-schema HMR on the same published Syncular runtime,
159
- and awaits old-resource disposal before creating a schema-bump or
160
- package-upgrade replacement. Request `hot.invalidate()` only when its
161
- `ownerChanged` result is true and `disposalError` is absent; the official
162
- example, optimizer exclusion, and upgrade workflow are in the
163
- [Vite guide](https://syncular.dev/guide-vite/). The helper may close
156
+ For Vite, use `createViteSyncClientResource(hot.data, schema.version,
157
+ createClient)`. It synchronously returns a pending-or-retained resource, keeps
158
+ a `{ schemaVersion, runtimeVersion, resource }` record, reuses it for
159
+ same-schema HMR on the same published Syncular runtime, and sequences a
160
+ schema-bump or package-upgrade factory behind old-resource disposal. Await its
161
+ `handoff` promise before requesting `hot.invalidate()` when `ownerChanged` is
162
+ true. A failed handoff is also exposed by the resource through the ordinary
163
+ startup boundary and never opens a second owner. This recipe requires no
164
+ top-level await; the official example, optimizer exclusion, and upgrade
165
+ workflow are in the [Vite guide](https://syncular.dev/guide-vite/).
166
+ `retainViteSyncClientResource` remains the async alternative for targets that
167
+ deliberately support top-level await. Either helper may close
164
168
  the retained client before React runs cleanup for the old provider; Syncular
165
169
  absorbs that teardown-only window-release race while still surfacing failures
166
170
  from closing the resource itself.
@@ -260,6 +264,16 @@ not need a custom retention effect.
260
264
 
261
265
  The hooks are SSR-safe: no local query runs during server rendering.
262
266
 
267
+ ### Router scheduling
268
+
269
+ Syncular hooks publish through `useSyncExternalStore`. If React Router owns
270
+ clinical query state, keep the router as the only owner and use its explicit
271
+ synchronous publication policy (`<RouterProvider useTransitions={false}>`) on
272
+ versions where sustained external-store updates can starve a transition. Do
273
+ not repair URL/render disagreement with `window.location`, `flushSync`, or a
274
+ mirrored store. The maintained fixture and upgrade guidance live in the
275
+ [React guide](https://syncular.dev/platform-react/#router-transition-scheduling).
276
+
263
277
  ### Privacy-safe support view
264
278
 
265
279
  Use application-owned, stable, PHI-free subscription ids to make missing
package/dist/index.d.ts CHANGED
@@ -23,4 +23,4 @@ export { type NamedQueryDescriptor, useQuery, } from './use-query.js';
23
23
  export { type UseRawSqlOptions, type UseRawSqlResult, useRawSql, } from './use-raw-sql.js';
24
24
  export { type SyncStatus, useSyncStatus } from './use-sync-status.js';
25
25
  export { type UseRetainedWindowResult, type UseWindowResult, useRetainedWindow, useWindow, } from './use-window.js';
26
- export { type RetainedSyncularResource, retainViteSyncClientResource, type ViteSyncClientResourceResult, } from './vite-hmr.js';
26
+ export { createViteSyncClientResource, type RetainedSyncularResource, retainViteSyncClientResource, type ViteSyncClientResourceBootstrap, type ViteSyncClientResourceResult, } from './vite-hmr.js';
package/dist/index.js CHANGED
@@ -16,4 +16,4 @@ export { useQuery, } from './use-query.js';
16
16
  export { useRawSql, } from './use-raw-sql.js';
17
17
  export { useSyncStatus } from './use-sync-status.js';
18
18
  export { useRetainedWindow, useWindow, } from './use-window.js';
19
- export { retainViteSyncClientResource, } from './vite-hmr.js';
19
+ export { createViteSyncClientResource, retainViteSyncClientResource, } from './vite-hmr.js';
@@ -3,4 +3,4 @@
3
3
  * Vite HMR uses it to replace a worker owner after a Syncular package upgrade
4
4
  * even when the application's generated schema version did not change.
5
5
  */
6
- export declare const SYNCULAR_REACT_RUNTIME_VERSION = "0.15.40";
6
+ export declare const SYNCULAR_REACT_RUNTIME_VERSION = "0.15.43";
@@ -3,4 +3,4 @@
3
3
  * Vite HMR uses it to replace a worker owner after a Syncular package upgrade
4
4
  * even when the application's generated schema version did not change.
5
5
  */
6
- export const SYNCULAR_REACT_RUNTIME_VERSION = '0.15.40';
6
+ export const SYNCULAR_REACT_RUNTIME_VERSION = '0.15.43';
@@ -20,7 +20,36 @@ export interface ViteSyncClientResourceResult {
20
20
  /** A failed close is exposed by `resource` as a startup error. */
21
21
  readonly disposalError?: Error;
22
22
  }
23
+ /**
24
+ * Synchronous module-bootstrap result for Vite targets that do not support
25
+ * top-level await. The resource is immediately usable by `SyncProvider`; it
26
+ * remains pending until an obsolete owner has closed and the replacement
27
+ * factory is allowed to run.
28
+ */
29
+ export interface ViteSyncClientResourceBootstrap {
30
+ readonly resource: SyncClientResource;
31
+ /**
32
+ * Resolves after an incompatible retained owner has closed. A rejection is
33
+ * also published by `resource` as its startup error, and prevents the
34
+ * replacement factory from running.
35
+ */
36
+ readonly handoff: Promise<void>;
37
+ /** Compatibility alias; true whenever the retained owner was replaced. */
38
+ readonly schemaChanged: boolean;
39
+ /** True only when a retained owner came from another Syncular release. */
40
+ readonly runtimeChanged: boolean;
41
+ /** True whenever an incompatible retained owner is being replaced. */
42
+ readonly ownerChanged: boolean;
43
+ }
23
44
  type HotData = Record<string, unknown>;
45
+ /**
46
+ * Create or reuse a Vite-retained resource without top-level await.
47
+ *
48
+ * On an owner change, the returned resource's factory is sequenced behind the
49
+ * prior resource's complete disposal. A failed close therefore becomes the
50
+ * ordinary resource startup error and never constructs a competing owner.
51
+ */
52
+ export declare function createViteSyncClientResource(hotData: HotData | undefined, schemaVersion: number, factory: () => SyncClientLike | Promise<SyncClientLike>, runtimeVersion?: string): ViteSyncClientResourceBootstrap;
24
53
  /**
25
54
  * Reuse a Vite-owned client only while both its captured generated schema and
26
55
  * materialized Syncular runtime identity match. On either change, the prior
package/dist/vite-hmr.js CHANGED
@@ -30,13 +30,7 @@ function retainedFrom(hotData) {
30
30
  resource: candidate.resource,
31
31
  };
32
32
  }
33
- /**
34
- * Reuse a Vite-owned client only while both its captured generated schema and
35
- * materialized Syncular runtime identity match. On either change, the prior
36
- * resource is fully disposed before the replacement resource (and therefore
37
- * its worker) is constructed.
38
- */
39
- export async function retainViteSyncClientResource(hotData, schemaVersion, factory, runtimeVersion = SYNCULAR_REACT_RUNTIME_VERSION) {
33
+ function validateIdentity(schemaVersion, runtimeVersion) {
40
34
  if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
41
35
  throw new TypeError('schemaVersion must be a positive integer');
42
36
  }
@@ -45,6 +39,64 @@ export async function retainViteSyncClientResource(hotData, schemaVersion, facto
45
39
  runtimeVersion.length > 96) {
46
40
  throw new TypeError('runtimeVersion must be a bounded non-empty string');
47
41
  }
42
+ }
43
+ /**
44
+ * Create or reuse a Vite-retained resource without top-level await.
45
+ *
46
+ * On an owner change, the returned resource's factory is sequenced behind the
47
+ * prior resource's complete disposal. A failed close therefore becomes the
48
+ * ordinary resource startup error and never constructs a competing owner.
49
+ */
50
+ export function createViteSyncClientResource(hotData, schemaVersion, factory, runtimeVersion = SYNCULAR_REACT_RUNTIME_VERSION) {
51
+ validateIdentity(schemaVersion, runtimeVersion);
52
+ const retained = retainedFrom(hotData);
53
+ if (retained?.schemaVersion === schemaVersion &&
54
+ retained.runtimeVersion === runtimeVersion) {
55
+ return {
56
+ resource: retained.resource,
57
+ handoff: Promise.resolve(),
58
+ schemaChanged: false,
59
+ runtimeChanged: false,
60
+ ownerChanged: false,
61
+ };
62
+ }
63
+ const ownerChanged = retained !== undefined;
64
+ const runtimeChanged = retained !== undefined && retained.runtimeVersion !== runtimeVersion;
65
+ const handoff = retained
66
+ ? Promise.resolve().then(() => retained.resource.dispose())
67
+ : Promise.resolve();
68
+ // The resource below observes the same rejection. Attaching a handler here
69
+ // also keeps an integration that ignores `handoff` free of an unhandled
70
+ // promise while the provider reports the typed startup failure.
71
+ void handoff.catch(() => undefined);
72
+ const resource = createSyncClientResource(async () => {
73
+ await handoff;
74
+ return factory();
75
+ });
76
+ if (hotData !== undefined) {
77
+ const record = {
78
+ schemaVersion,
79
+ runtimeVersion,
80
+ resource,
81
+ };
82
+ hotData.syncularClientResource = record;
83
+ }
84
+ return {
85
+ resource,
86
+ handoff,
87
+ schemaChanged: ownerChanged,
88
+ runtimeChanged,
89
+ ownerChanged,
90
+ };
91
+ }
92
+ /**
93
+ * Reuse a Vite-owned client only while both its captured generated schema and
94
+ * materialized Syncular runtime identity match. On either change, the prior
95
+ * resource is fully disposed before the replacement resource (and therefore
96
+ * its worker) is constructed.
97
+ */
98
+ export async function retainViteSyncClientResource(hotData, schemaVersion, factory, runtimeVersion = SYNCULAR_REACT_RUNTIME_VERSION) {
99
+ validateIdentity(schemaVersion, runtimeVersion);
48
100
  const retained = retainedFrom(hotData);
49
101
  if (retained?.schemaVersion === schemaVersion &&
50
102
  retained.runtimeVersion === runtimeVersion) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/react",
3
- "version": "0.15.40",
3
+ "version": "0.15.43",
4
4
  "description": "React hooks for Syncular offline-first sync",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -55,18 +55,20 @@
55
55
  "test": "bun test --preload ./test/setup.ts"
56
56
  },
57
57
  "dependencies": {
58
- "@syncular/client": "0.15.40"
58
+ "@syncular/client": "0.15.43"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "react": ">=18.0.0"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@happy-dom/global-registrator": "^20.0.0",
65
- "@syncular/core": "0.15.40",
66
- "@syncular/server": "0.15.40",
65
+ "@syncular/core": "0.15.43",
66
+ "@syncular/server": "0.15.43",
67
67
  "@testing-library/react": "^16.1.0",
68
68
  "@types/react": "^18.3.0",
69
69
  "react": "^18.3.1",
70
- "react-dom": "^18.3.1"
70
+ "react-dom": "^18.3.1",
71
+ "react-router-dom": "^7.18.1",
72
+ "vite": "^8.1.3"
71
73
  }
72
74
  }
package/src/index.ts CHANGED
@@ -67,7 +67,9 @@ export {
67
67
  useWindow,
68
68
  } from './use-window';
69
69
  export {
70
+ createViteSyncClientResource,
70
71
  type RetainedSyncularResource,
71
72
  retainViteSyncClientResource,
73
+ type ViteSyncClientResourceBootstrap,
72
74
  type ViteSyncClientResourceResult,
73
75
  } from './vite-hmr';
@@ -3,4 +3,4 @@
3
3
  * Vite HMR uses it to replace a worker owner after a Syncular package upgrade
4
4
  * even when the application's generated schema version did not change.
5
5
  */
6
- export const SYNCULAR_REACT_RUNTIME_VERSION = '0.15.40';
6
+ export const SYNCULAR_REACT_RUNTIME_VERSION = '0.15.43';
package/src/vite-hmr.ts CHANGED
@@ -24,6 +24,28 @@ export interface ViteSyncClientResourceResult {
24
24
  readonly disposalError?: Error;
25
25
  }
26
26
 
27
+ /**
28
+ * Synchronous module-bootstrap result for Vite targets that do not support
29
+ * top-level await. The resource is immediately usable by `SyncProvider`; it
30
+ * remains pending until an obsolete owner has closed and the replacement
31
+ * factory is allowed to run.
32
+ */
33
+ export interface ViteSyncClientResourceBootstrap {
34
+ readonly resource: SyncClientResource;
35
+ /**
36
+ * Resolves after an incompatible retained owner has closed. A rejection is
37
+ * also published by `resource` as its startup error, and prevents the
38
+ * replacement factory from running.
39
+ */
40
+ readonly handoff: Promise<void>;
41
+ /** Compatibility alias; true whenever the retained owner was replaced. */
42
+ readonly schemaChanged: boolean;
43
+ /** True only when a retained owner came from another Syncular release. */
44
+ readonly runtimeChanged: boolean;
45
+ /** True whenever an incompatible retained owner is being replaced. */
46
+ readonly ownerChanged: boolean;
47
+ }
48
+
27
49
  type HotData = Record<string, unknown>;
28
50
 
29
51
  function errorOf(value: unknown): Error {
@@ -72,6 +94,81 @@ function retainedFrom(hotData: HotData | undefined):
72
94
  };
73
95
  }
74
96
 
97
+ function validateIdentity(schemaVersion: number, runtimeVersion: string): void {
98
+ if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
99
+ throw new TypeError('schemaVersion must be a positive integer');
100
+ }
101
+ if (
102
+ typeof runtimeVersion !== 'string' ||
103
+ runtimeVersion.length === 0 ||
104
+ runtimeVersion.length > 96
105
+ ) {
106
+ throw new TypeError('runtimeVersion must be a bounded non-empty string');
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Create or reuse a Vite-retained resource without top-level await.
112
+ *
113
+ * On an owner change, the returned resource's factory is sequenced behind the
114
+ * prior resource's complete disposal. A failed close therefore becomes the
115
+ * ordinary resource startup error and never constructs a competing owner.
116
+ */
117
+ export function createViteSyncClientResource(
118
+ hotData: HotData | undefined,
119
+ schemaVersion: number,
120
+ factory: () => SyncClientLike | Promise<SyncClientLike>,
121
+ runtimeVersion = SYNCULAR_REACT_RUNTIME_VERSION,
122
+ ): ViteSyncClientResourceBootstrap {
123
+ validateIdentity(schemaVersion, runtimeVersion);
124
+
125
+ const retained = retainedFrom(hotData);
126
+ if (
127
+ retained?.schemaVersion === schemaVersion &&
128
+ retained.runtimeVersion === runtimeVersion
129
+ ) {
130
+ return {
131
+ resource: retained.resource,
132
+ handoff: Promise.resolve(),
133
+ schemaChanged: false,
134
+ runtimeChanged: false,
135
+ ownerChanged: false,
136
+ };
137
+ }
138
+
139
+ const ownerChanged = retained !== undefined;
140
+ const runtimeChanged =
141
+ retained !== undefined && retained.runtimeVersion !== runtimeVersion;
142
+ const handoff = retained
143
+ ? Promise.resolve().then(() => retained.resource.dispose())
144
+ : Promise.resolve();
145
+ // The resource below observes the same rejection. Attaching a handler here
146
+ // also keeps an integration that ignores `handoff` free of an unhandled
147
+ // promise while the provider reports the typed startup failure.
148
+ void handoff.catch(() => undefined);
149
+ const resource = createSyncClientResource(async () => {
150
+ await handoff;
151
+ return factory();
152
+ });
153
+
154
+ if (hotData !== undefined) {
155
+ const record: RetainedSyncularResource = {
156
+ schemaVersion,
157
+ runtimeVersion,
158
+ resource,
159
+ };
160
+ hotData.syncularClientResource = record;
161
+ }
162
+
163
+ return {
164
+ resource,
165
+ handoff,
166
+ schemaChanged: ownerChanged,
167
+ runtimeChanged,
168
+ ownerChanged,
169
+ };
170
+ }
171
+
75
172
  /**
76
173
  * Reuse a Vite-owned client only while both its captured generated schema and
77
174
  * materialized Syncular runtime identity match. On either change, the prior
@@ -84,16 +181,7 @@ export async function retainViteSyncClientResource(
84
181
  factory: () => SyncClientLike | Promise<SyncClientLike>,
85
182
  runtimeVersion = SYNCULAR_REACT_RUNTIME_VERSION,
86
183
  ): Promise<ViteSyncClientResourceResult> {
87
- if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
88
- throw new TypeError('schemaVersion must be a positive integer');
89
- }
90
- if (
91
- typeof runtimeVersion !== 'string' ||
92
- runtimeVersion.length === 0 ||
93
- runtimeVersion.length > 96
94
- ) {
95
- throw new TypeError('runtimeVersion must be a bounded non-empty string');
96
- }
184
+ validateIdentity(schemaVersion, runtimeVersion);
97
185
 
98
186
  const retained = retainedFrom(hotData);
99
187
  if (