@syncular/client-react 0.0.6-171 → 0.0.6-178
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/dist/createSyncularReact-public.d.ts +6 -0
- package/dist/createSyncularReact-public.d.ts.map +1 -0
- package/dist/createSyncularReact-public.js +146 -0
- package/dist/createSyncularReact-public.js.map +1 -0
- package/dist/createSyncularReact.d.ts +14 -1
- package/dist/createSyncularReact.d.ts.map +1 -1
- package/dist/createSyncularReact.js +99 -3
- package/dist/createSyncularReact.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/hooks.test.tsx +148 -0
- package/src/__tests__/useSyncQuery.branching.test.tsx +155 -0
- package/src/__tests__/useSyncQuery.structural-sharing.test.tsx +134 -0
- package/src/createSyncularReact-public.tsx +220 -0
- package/src/createSyncularReact.tsx +147 -2
- package/src/index.ts +1 -1
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
createQueryContext,
|
|
41
41
|
enqueueOutboxCommit,
|
|
42
42
|
FingerprintCollector,
|
|
43
|
+
type FingerprintMode,
|
|
43
44
|
type OutboxStats,
|
|
44
45
|
type PresenceEntry,
|
|
45
46
|
type QueryContext,
|
|
@@ -70,6 +71,125 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
70
71
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
function shallowEqualRecords(
|
|
75
|
+
left: Record<string, unknown>,
|
|
76
|
+
right: Record<string, unknown>
|
|
77
|
+
): boolean {
|
|
78
|
+
if (left === right) return true;
|
|
79
|
+
|
|
80
|
+
const leftKeys = Object.keys(left);
|
|
81
|
+
const rightKeys = Object.keys(right);
|
|
82
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
83
|
+
|
|
84
|
+
for (const key of leftKeys) {
|
|
85
|
+
if (!(key in right)) return false;
|
|
86
|
+
if (!Object.is(left[key], right[key])) return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function shallowEqualQueryValues(left: unknown, right: unknown): boolean {
|
|
93
|
+
if (Object.is(left, right)) return true;
|
|
94
|
+
if (!isRecord(left) || !isRecord(right)) return false;
|
|
95
|
+
return shallowEqualRecords(left, right);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getKeyedQueryValueKey<T>(value: T, keyField: string): string | null {
|
|
99
|
+
if (!isRecord(value) || !(keyField in value)) return null;
|
|
100
|
+
|
|
101
|
+
const key = value[keyField];
|
|
102
|
+
if (key === null || key === undefined) return null;
|
|
103
|
+
return String(key);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function shareArrayResult<T>(previous: T[], next: T[], keyField: string): T[] {
|
|
107
|
+
if (previous.length === 0 || next.length === 0) {
|
|
108
|
+
return next;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const previousByKey = new Map<string, T>();
|
|
112
|
+
let canShareByKey = true;
|
|
113
|
+
|
|
114
|
+
for (const item of previous) {
|
|
115
|
+
const key = getKeyedQueryValueKey(item, keyField);
|
|
116
|
+
if (key === null) {
|
|
117
|
+
canShareByKey = false;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
previousByKey.set(key, item);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const shared = next.slice();
|
|
124
|
+
|
|
125
|
+
if (canShareByKey) {
|
|
126
|
+
for (const [index, item] of next.entries()) {
|
|
127
|
+
const key = getKeyedQueryValueKey(item, keyField);
|
|
128
|
+
if (key === null) {
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const previousItem = previousByKey.get(key);
|
|
133
|
+
if (
|
|
134
|
+
previousItem !== undefined &&
|
|
135
|
+
shallowEqualQueryValues(previousItem, item)
|
|
136
|
+
) {
|
|
137
|
+
shared[index] = previousItem;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
const limit = Math.min(previous.length, next.length);
|
|
142
|
+
for (let index = 0; index < limit; index += 1) {
|
|
143
|
+
const previousItem = previous[index];
|
|
144
|
+
const nextItem = next[index];
|
|
145
|
+
if (
|
|
146
|
+
previousItem !== undefined &&
|
|
147
|
+
nextItem !== undefined &&
|
|
148
|
+
shallowEqualQueryValues(previousItem, nextItem)
|
|
149
|
+
) {
|
|
150
|
+
shared[index] = previousItem;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (shared.length !== previous.length) {
|
|
156
|
+
return shared;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (let index = 0; index < shared.length; index += 1) {
|
|
160
|
+
if (!Object.is(shared[index], previous[index])) {
|
|
161
|
+
return shared;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return previous;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function shareQueryResult<TResult>(
|
|
169
|
+
previous: TResult | undefined,
|
|
170
|
+
next: TResult,
|
|
171
|
+
keyField: string,
|
|
172
|
+
enabled: boolean
|
|
173
|
+
): TResult {
|
|
174
|
+
if (!enabled || previous === undefined) {
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (Array.isArray(previous) && Array.isArray(next)) {
|
|
179
|
+
return shareArrayResult(previous, next, keyField) as TResult;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
isRecord(previous) &&
|
|
184
|
+
isRecord(next) &&
|
|
185
|
+
shallowEqualRecords(previous, next)
|
|
186
|
+
) {
|
|
187
|
+
return previous;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return next;
|
|
191
|
+
}
|
|
192
|
+
|
|
73
193
|
type ExecutableQuery<TResult> = {
|
|
74
194
|
execute: () => Promise<TResult>;
|
|
75
195
|
};
|
|
@@ -372,8 +492,21 @@ export interface UseSyncQueryOptions {
|
|
|
372
492
|
deps?: unknown[];
|
|
373
493
|
keyField?: string;
|
|
374
494
|
watchTables?: string[];
|
|
495
|
+
/**
|
|
496
|
+
* Fingerprint strategy used to decide whether a refreshed query result should
|
|
497
|
+
* publish new data.
|
|
498
|
+
* - `auto` (default): row-based fingerprints for single-table keyed arrays,
|
|
499
|
+
* value-based fingerprints otherwise.
|
|
500
|
+
* - `value`: always fingerprint the full result value.
|
|
501
|
+
*/
|
|
502
|
+
fingerprintMode?: FingerprintMode;
|
|
375
503
|
pollIntervalMs?: number;
|
|
376
504
|
staleAfterMs?: number;
|
|
505
|
+
/**
|
|
506
|
+
* Reuse unchanged object references between query refreshes.
|
|
507
|
+
* Enabled by default for safer list rendering.
|
|
508
|
+
*/
|
|
509
|
+
structuralSharing?: boolean;
|
|
377
510
|
/**
|
|
378
511
|
* Internal: if false, skip `data:change` invalidation listener.
|
|
379
512
|
* Used by `useQuery` wrapper.
|
|
@@ -1299,8 +1432,10 @@ export function createSyncularReact<
|
|
|
1299
1432
|
deps = [],
|
|
1300
1433
|
keyField = 'id',
|
|
1301
1434
|
watchTables = [],
|
|
1435
|
+
fingerprintMode = 'auto',
|
|
1302
1436
|
pollIntervalMs,
|
|
1303
1437
|
staleAfterMs,
|
|
1438
|
+
structuralSharing = true,
|
|
1304
1439
|
refreshOnDataChange = true,
|
|
1305
1440
|
loadingOnRefresh = false,
|
|
1306
1441
|
transitionUpdates = true,
|
|
@@ -1389,7 +1524,8 @@ export function createSyncularReact<
|
|
|
1389
1524
|
scopeCollector,
|
|
1390
1525
|
fingerprintCollectorRef.current,
|
|
1391
1526
|
engine,
|
|
1392
|
-
keyField
|
|
1527
|
+
keyField,
|
|
1528
|
+
fingerprintMode
|
|
1393
1529
|
);
|
|
1394
1530
|
|
|
1395
1531
|
const fnResult = queryFnRef.current(ctx);
|
|
@@ -1415,7 +1551,14 @@ export function createSyncularReact<
|
|
|
1415
1551
|
() => {
|
|
1416
1552
|
setLastSyncAt(snapshotLastSyncAt);
|
|
1417
1553
|
if (didFingerprintChange) {
|
|
1418
|
-
setData(
|
|
1554
|
+
setData((previous) =>
|
|
1555
|
+
shareQueryResult(
|
|
1556
|
+
previous,
|
|
1557
|
+
result,
|
|
1558
|
+
keyField,
|
|
1559
|
+
structuralSharing
|
|
1560
|
+
)
|
|
1561
|
+
);
|
|
1419
1562
|
}
|
|
1420
1563
|
setError(null);
|
|
1421
1564
|
},
|
|
@@ -1449,8 +1592,10 @@ export function createSyncularReact<
|
|
|
1449
1592
|
db,
|
|
1450
1593
|
enabled,
|
|
1451
1594
|
engine,
|
|
1595
|
+
fingerprintMode,
|
|
1452
1596
|
keyField,
|
|
1453
1597
|
loadingOnRefresh,
|
|
1598
|
+
structuralSharing,
|
|
1454
1599
|
applyUpdate,
|
|
1455
1600
|
emitMetrics,
|
|
1456
1601
|
]
|
package/src/index.ts
CHANGED
|
@@ -46,7 +46,7 @@ export type {
|
|
|
46
46
|
UseTransportHealthResult,
|
|
47
47
|
} from './createSyncularReact';
|
|
48
48
|
// Re-export core client types for convenience.
|
|
49
|
-
export { createSyncularReact } from './createSyncularReact';
|
|
49
|
+
export { createSyncularReact } from './createSyncularReact-public';
|
|
50
50
|
export type { UseCachedAsyncValueOptions } from './use-cached-async-value';
|
|
51
51
|
export {
|
|
52
52
|
clearCachedAsyncValues,
|