@syncular/react 0.15.29 → 0.15.30

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
@@ -154,11 +154,13 @@ 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
156
  For Vite, use `retainViteSyncClientResource(hot.data, schema.version,
157
- createClient)`. It retains a `{ schemaVersion, resource }` record, reuses it for
158
- same-schema HMR, and awaits old-resource disposal before creating a schema-bump
159
- replacement. Request `hot.invalidate()` only when its `schemaChanged` result is
160
- true and `disposalError` is absent; the official example and full explanation
161
- are in the [Vite guide](https://syncular.dev/guide-vite/). The helper may close
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
162
164
  the retained client before React runs cleanup for the old provider; Syncular
163
165
  absorbs that teardown-only window-release race while still surfacing failures
164
166
  from closing the resource itself.
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export { normalizeClient } from './client.js';
12
12
  export { inferTables } from './infer-tables.js';
13
13
  export { type SyncBoundaryActions, type SyncBoundaryState, SyncContext, SyncProvider, type SyncProviderProps, SyncStoreContext, } from './provider.js';
14
14
  export { createSyncClientResource, isSyncClientResource, type SyncClientResource, type SyncClientResourceSnapshot, } from './resource.js';
15
+ export { SYNCULAR_REACT_RUNTIME_VERSION } from './runtime-version.js';
15
16
  export { useReactiveStore, useSyncClient } from './use-client.js';
16
17
  export { type UseCommitOutcomesResult, useCommitOutcomes, } from './use-commit-outcomes.js';
17
18
  export { type UseConflictsResult, useConflicts } from './use-conflicts.js';
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ export { normalizeClient } from './client.js';
5
5
  export { inferTables } from './infer-tables.js';
6
6
  export { SyncContext, SyncProvider, SyncStoreContext, } from './provider.js';
7
7
  export { createSyncClientResource, isSyncClientResource, } from './resource.js';
8
+ export { SYNCULAR_REACT_RUNTIME_VERSION } from './runtime-version.js';
8
9
  export { useReactiveStore, useSyncClient } from './use-client.js';
9
10
  export { useCommitOutcomes, } from './use-commit-outcomes.js';
10
11
  export { useConflicts } from './use-conflicts.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Materialized from the repository release version by scripts/version.ts.
3
+ * Vite HMR uses it to replace a worker owner after a Syncular package upgrade
4
+ * even when the application's generated schema version did not change.
5
+ */
6
+ export declare const SYNCULAR_REACT_RUNTIME_VERSION = "0.15.30";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Materialized from the repository release version by scripts/version.ts.
3
+ * Vite HMR uses it to replace a worker owner after a Syncular package upgrade
4
+ * even when the application's generated schema version did not change.
5
+ */
6
+ export const SYNCULAR_REACT_RUNTIME_VERSION = '0.15.30';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * First-party Syncular browser packages are already ESM. Keeping them out of
3
+ * Vite's dependency optimizer prevents a live worker from referring to a
4
+ * retired hashed optimizer chunk after a package upgrade.
5
+ */
6
+ export declare const SYNCULAR_VITE_OPTIMIZE_DEPS_EXCLUDE: readonly ["@syncular/client", "@syncular/client/worker", "@syncular/core", "@syncular/crypto", "@syncular/react"];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * First-party Syncular browser packages are already ESM. Keeping them out of
3
+ * Vite's dependency optimizer prevents a live worker from referring to a
4
+ * retired hashed optimizer chunk after a package upgrade.
5
+ */
6
+ export const SYNCULAR_VITE_OPTIMIZE_DEPS_EXCLUDE = [
7
+ '@syncular/client',
8
+ '@syncular/client/worker',
9
+ '@syncular/core',
10
+ '@syncular/crypto',
11
+ '@syncular/react',
12
+ ];
@@ -3,20 +3,29 @@ import { type SyncClientResource } from './resource.js';
3
3
  /** The exact value retained in `import.meta.hot.data.syncularClientResource`. */
4
4
  export interface RetainedSyncularResource {
5
5
  readonly schemaVersion: number;
6
+ readonly runtimeVersion: string;
6
7
  readonly resource: SyncClientResource;
7
8
  }
8
9
  export interface ViteSyncClientResourceResult {
9
10
  readonly resource: SyncClientResource;
10
- /** True only when an older captured schema identity was replaced. */
11
+ /**
12
+ * Compatibility signal used by existing integrations to invalidate HMR.
13
+ * True when either the schema or Syncular runtime identity changed.
14
+ */
11
15
  readonly schemaChanged: boolean;
16
+ /** True only when a retained owner came from another Syncular release. */
17
+ readonly runtimeChanged: boolean;
18
+ /** True whenever an incompatible retained owner was replaced. */
19
+ readonly ownerChanged: boolean;
12
20
  /** A failed close is exposed by `resource` as a startup error. */
13
21
  readonly disposalError?: Error;
14
22
  }
15
23
  type HotData = Record<string, unknown>;
16
24
  /**
17
- * Reuse a Vite-owned client only while its captured generated schema matches.
18
- * On a bump, the prior resource is fully disposed before the replacement
19
- * resource (and therefore its worker) is constructed.
25
+ * Reuse a Vite-owned client only while both its captured generated schema and
26
+ * materialized Syncular runtime identity match. On either change, the prior
27
+ * resource is fully disposed before the replacement resource (and therefore
28
+ * its worker) is constructed.
20
29
  */
21
- export declare function retainViteSyncClientResource(hotData: HotData | undefined, schemaVersion: number, factory: () => SyncClientLike | Promise<SyncClientLike>): Promise<ViteSyncClientResourceResult>;
30
+ export declare function retainViteSyncClientResource(hotData: HotData | undefined, schemaVersion: number, factory: () => SyncClientLike | Promise<SyncClientLike>, runtimeVersion?: string): Promise<ViteSyncClientResourceResult>;
22
31
  export {};
package/dist/vite-hmr.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createSyncClientResource } from './resource.js';
2
+ import { SYNCULAR_REACT_RUNTIME_VERSION } from './runtime-version.js';
2
3
  function errorOf(value) {
3
4
  return value instanceof Error ? value : new Error(String(value));
4
5
  }
@@ -23,22 +24,39 @@ function retainedFrom(hotData) {
23
24
  }
24
25
  return {
25
26
  schemaVersion: candidate.schemaVersion,
27
+ ...(typeof candidate.runtimeVersion === 'string'
28
+ ? { runtimeVersion: candidate.runtimeVersion }
29
+ : {}),
26
30
  resource: candidate.resource,
27
31
  };
28
32
  }
29
33
  /**
30
- * Reuse a Vite-owned client only while its captured generated schema matches.
31
- * On a bump, the prior resource is fully disposed before the replacement
32
- * resource (and therefore its worker) is constructed.
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.
33
38
  */
34
- export async function retainViteSyncClientResource(hotData, schemaVersion, factory) {
39
+ export async function retainViteSyncClientResource(hotData, schemaVersion, factory, runtimeVersion = SYNCULAR_REACT_RUNTIME_VERSION) {
35
40
  if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
36
41
  throw new TypeError('schemaVersion must be a positive integer');
37
42
  }
43
+ if (typeof runtimeVersion !== 'string' ||
44
+ runtimeVersion.length === 0 ||
45
+ runtimeVersion.length > 96) {
46
+ throw new TypeError('runtimeVersion must be a bounded non-empty string');
47
+ }
38
48
  const retained = retainedFrom(hotData);
39
- if (retained?.schemaVersion === schemaVersion) {
40
- return { resource: retained.resource, schemaChanged: false };
49
+ if (retained?.schemaVersion === schemaVersion &&
50
+ retained.runtimeVersion === runtimeVersion) {
51
+ return {
52
+ resource: retained.resource,
53
+ schemaChanged: false,
54
+ runtimeChanged: false,
55
+ ownerChanged: false,
56
+ };
41
57
  }
58
+ const ownerChanged = retained !== undefined;
59
+ const runtimeChanged = retained !== undefined && retained.runtimeVersion !== runtimeVersion;
42
60
  let disposalError;
43
61
  if (retained !== undefined) {
44
62
  try {
@@ -52,12 +70,20 @@ export async function retainViteSyncClientResource(hotData, schemaVersion, facto
52
70
  // whose ordinary provider startup boundary reports the close failure.
53
71
  const resource = createSyncClientResource(disposalError === undefined ? factory : () => Promise.reject(disposalError));
54
72
  if (hotData !== undefined) {
55
- const record = { schemaVersion, resource };
73
+ const record = {
74
+ schemaVersion,
75
+ runtimeVersion,
76
+ resource,
77
+ };
56
78
  hotData.syncularClientResource = record;
57
79
  }
58
80
  return {
59
81
  resource,
60
- schemaChanged: retained !== undefined,
82
+ // Existing apps already invalidate on this property. Preserve that
83
+ // recovery behavior for a same-schema package upgrade.
84
+ schemaChanged: ownerChanged,
85
+ runtimeChanged,
86
+ ownerChanged,
61
87
  ...(disposalError !== undefined ? { disposalError } : {}),
62
88
  };
63
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/react",
3
- "version": "0.15.29",
3
+ "version": "0.15.30",
4
4
  "description": "React hooks for Syncular offline-first sync",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -33,6 +33,13 @@
33
33
  "types": "./dist/index.d.ts",
34
34
  "default": "./dist/index.js"
35
35
  }
36
+ },
37
+ "./vite": {
38
+ "bun": "./src/vite-config.ts",
39
+ "import": {
40
+ "types": "./dist/vite-config.d.ts",
41
+ "default": "./dist/vite-config.js"
42
+ }
36
43
  }
37
44
  },
38
45
  "files": [
@@ -48,15 +55,15 @@
48
55
  "test": "bun test --preload ./test/setup.ts"
49
56
  },
50
57
  "dependencies": {
51
- "@syncular/client": "0.15.29"
58
+ "@syncular/client": "0.15.30"
52
59
  },
53
60
  "peerDependencies": {
54
61
  "react": ">=18.0.0"
55
62
  },
56
63
  "devDependencies": {
57
64
  "@happy-dom/global-registrator": "^20.0.0",
58
- "@syncular/core": "0.15.29",
59
- "@syncular/server": "0.15.29",
65
+ "@syncular/core": "0.15.30",
66
+ "@syncular/server": "0.15.30",
60
67
  "@testing-library/react": "^16.1.0",
61
68
  "@types/react": "^18.3.0",
62
69
  "react": "^18.3.1",
package/src/index.ts CHANGED
@@ -30,6 +30,7 @@ export {
30
30
  type SyncClientResource,
31
31
  type SyncClientResourceSnapshot,
32
32
  } from './resource';
33
+ export { SYNCULAR_REACT_RUNTIME_VERSION } from './runtime-version';
33
34
  export { useReactiveStore, useSyncClient } from './use-client';
34
35
  export {
35
36
  type UseCommitOutcomesResult,
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Materialized from the repository release version by scripts/version.ts.
3
+ * Vite HMR uses it to replace a worker owner after a Syncular package upgrade
4
+ * even when the application's generated schema version did not change.
5
+ */
6
+ export const SYNCULAR_REACT_RUNTIME_VERSION = '0.15.30';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * First-party Syncular browser packages are already ESM. Keeping them out of
3
+ * Vite's dependency optimizer prevents a live worker from referring to a
4
+ * retired hashed optimizer chunk after a package upgrade.
5
+ */
6
+ export const SYNCULAR_VITE_OPTIMIZE_DEPS_EXCLUDE = [
7
+ '@syncular/client',
8
+ '@syncular/client/worker',
9
+ '@syncular/core',
10
+ '@syncular/crypto',
11
+ '@syncular/react',
12
+ ] as const;
package/src/vite-hmr.ts CHANGED
@@ -1,16 +1,25 @@
1
1
  import type { SyncClientLike } from './client';
2
2
  import { createSyncClientResource, type SyncClientResource } from './resource';
3
+ import { SYNCULAR_REACT_RUNTIME_VERSION } from './runtime-version';
3
4
 
4
5
  /** The exact value retained in `import.meta.hot.data.syncularClientResource`. */
5
6
  export interface RetainedSyncularResource {
6
7
  readonly schemaVersion: number;
8
+ readonly runtimeVersion: string;
7
9
  readonly resource: SyncClientResource;
8
10
  }
9
11
 
10
12
  export interface ViteSyncClientResourceResult {
11
13
  readonly resource: SyncClientResource;
12
- /** True only when an older captured schema identity was replaced. */
14
+ /**
15
+ * Compatibility signal used by existing integrations to invalidate HMR.
16
+ * True when either the schema or Syncular runtime identity changed.
17
+ */
13
18
  readonly schemaChanged: boolean;
19
+ /** True only when a retained owner came from another Syncular release. */
20
+ readonly runtimeChanged: boolean;
21
+ /** True whenever an incompatible retained owner was replaced. */
22
+ readonly ownerChanged: boolean;
14
23
  /** A failed close is exposed by `resource` as a startup error. */
15
24
  readonly disposalError?: Error;
16
25
  }
@@ -29,10 +38,12 @@ function isResource(value: unknown): value is SyncClientResource {
29
38
  );
30
39
  }
31
40
 
32
- function retainedFrom(
33
- hotData: HotData | undefined,
34
- ):
35
- | { readonly schemaVersion?: number; readonly resource: SyncClientResource }
41
+ function retainedFrom(hotData: HotData | undefined):
42
+ | {
43
+ readonly schemaVersion?: number;
44
+ readonly runtimeVersion?: string;
45
+ readonly resource: SyncClientResource;
46
+ }
36
47
  | undefined {
37
48
  const value = hotData?.syncularClientResource;
38
49
  if (isResource(value)) {
@@ -42,6 +53,7 @@ function retainedFrom(
42
53
  if (typeof value !== 'object' || value === null) return undefined;
43
54
  const candidate = value as {
44
55
  readonly schemaVersion?: unknown;
56
+ readonly runtimeVersion?: unknown;
45
57
  readonly resource?: unknown;
46
58
  };
47
59
  if (
@@ -53,29 +65,53 @@ function retainedFrom(
53
65
  }
54
66
  return {
55
67
  schemaVersion: candidate.schemaVersion,
68
+ ...(typeof candidate.runtimeVersion === 'string'
69
+ ? { runtimeVersion: candidate.runtimeVersion }
70
+ : {}),
56
71
  resource: candidate.resource,
57
72
  };
58
73
  }
59
74
 
60
75
  /**
61
- * Reuse a Vite-owned client only while its captured generated schema matches.
62
- * On a bump, the prior resource is fully disposed before the replacement
63
- * resource (and therefore its worker) is constructed.
76
+ * Reuse a Vite-owned client only while both its captured generated schema and
77
+ * materialized Syncular runtime identity match. On either change, the prior
78
+ * resource is fully disposed before the replacement resource (and therefore
79
+ * its worker) is constructed.
64
80
  */
65
81
  export async function retainViteSyncClientResource(
66
82
  hotData: HotData | undefined,
67
83
  schemaVersion: number,
68
84
  factory: () => SyncClientLike | Promise<SyncClientLike>,
85
+ runtimeVersion = SYNCULAR_REACT_RUNTIME_VERSION,
69
86
  ): Promise<ViteSyncClientResourceResult> {
70
87
  if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
71
88
  throw new TypeError('schemaVersion must be a positive integer');
72
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
+ }
73
97
 
74
98
  const retained = retainedFrom(hotData);
75
- if (retained?.schemaVersion === schemaVersion) {
76
- return { resource: retained.resource, schemaChanged: false };
99
+ if (
100
+ retained?.schemaVersion === schemaVersion &&
101
+ retained.runtimeVersion === runtimeVersion
102
+ ) {
103
+ return {
104
+ resource: retained.resource,
105
+ schemaChanged: false,
106
+ runtimeChanged: false,
107
+ ownerChanged: false,
108
+ };
77
109
  }
78
110
 
111
+ const ownerChanged = retained !== undefined;
112
+ const runtimeChanged =
113
+ retained !== undefined && retained.runtimeVersion !== runtimeVersion;
114
+
79
115
  let disposalError: Error | undefined;
80
116
  if (retained !== undefined) {
81
117
  try {
@@ -91,12 +127,20 @@ export async function retainViteSyncClientResource(
91
127
  disposalError === undefined ? factory : () => Promise.reject(disposalError),
92
128
  );
93
129
  if (hotData !== undefined) {
94
- const record: RetainedSyncularResource = { schemaVersion, resource };
130
+ const record: RetainedSyncularResource = {
131
+ schemaVersion,
132
+ runtimeVersion,
133
+ resource,
134
+ };
95
135
  hotData.syncularClientResource = record;
96
136
  }
97
137
  return {
98
138
  resource,
99
- schemaChanged: retained !== undefined,
139
+ // Existing apps already invalidate on this property. Preserve that
140
+ // recovery behavior for a same-schema package upgrade.
141
+ schemaChanged: ownerChanged,
142
+ runtimeChanged,
143
+ ownerChanged,
100
144
  ...(disposalError !== undefined ? { disposalError } : {}),
101
145
  };
102
146
  }