on-zero 0.4.48 → 0.4.50
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/cjs/createUseQuery.cjs +29 -20
- package/dist/cjs/createUseQuery.native.js +27 -14
- package/dist/cjs/createUseQuery.native.js.map +1 -1
- package/dist/cjs/createUseQueryDirect.cjs +5 -1
- package/dist/cjs/createUseQueryDirect.native.js +5 -1
- package/dist/cjs/createUseQueryDirect.native.js.map +1 -1
- package/dist/cjs/useContentStable.cjs +66 -0
- package/dist/cjs/useContentStable.native.js +93 -0
- package/dist/cjs/useContentStable.native.js.map +1 -0
- package/dist/cjs/useContentStable.test.cjs +105 -0
- package/dist/cjs/useContentStable.test.native.js +108 -0
- package/dist/cjs/useContentStable.test.native.js.map +1 -0
- package/dist/esm/createUseQuery.mjs +29 -20
- package/dist/esm/createUseQuery.mjs.map +1 -1
- package/dist/esm/createUseQuery.native.js +27 -14
- package/dist/esm/createUseQuery.native.js.map +1 -1
- package/dist/esm/createUseQueryDirect.mjs +5 -1
- package/dist/esm/createUseQueryDirect.mjs.map +1 -1
- package/dist/esm/createUseQueryDirect.native.js +5 -1
- package/dist/esm/createUseQueryDirect.native.js.map +1 -1
- package/dist/esm/useContentStable.mjs +40 -0
- package/dist/esm/useContentStable.mjs.map +1 -0
- package/dist/esm/useContentStable.native.js +64 -0
- package/dist/esm/useContentStable.native.js.map +1 -0
- package/dist/esm/useContentStable.test.mjs +106 -0
- package/dist/esm/useContentStable.test.mjs.map +1 -0
- package/dist/esm/useContentStable.test.native.js +106 -0
- package/dist/esm/useContentStable.test.native.js.map +1 -0
- package/package.json +2 -2
- package/src/createUseQuery.tsx +44 -12
- package/src/createUseQueryDirect.tsx +10 -1
- package/src/useContentStable.test.ts +61 -0
- package/src/useContentStable.ts +78 -0
- package/types/createUseQuery.d.ts +2 -2
- package/types/createUseQuery.d.ts.map +1 -1
- package/types/createUseQueryDirect.d.ts.map +1 -1
- package/types/useContentStable.d.ts +3 -0
- package/types/useContentStable.d.ts.map +1 -0
- package/types/useContentStable.test.d.ts +2 -0
- package/types/useContentStable.test.d.ts.map +1 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isContentEqual } from "./useContentStable.native.js";
|
|
3
|
+
describe("isContentEqual", function () {
|
|
4
|
+
it("treats primitives by value", function () {
|
|
5
|
+
expect(isContentEqual(1, 1)).toBe(true);
|
|
6
|
+
expect(isContentEqual("a", "a")).toBe(true);
|
|
7
|
+
expect(isContentEqual(true, true)).toBe(true);
|
|
8
|
+
expect(isContentEqual(void 0, void 0)).toBe(true);
|
|
9
|
+
expect(isContentEqual(null, null)).toBe(true);
|
|
10
|
+
expect(isContentEqual(1, 2)).toBe(false);
|
|
11
|
+
expect(isContentEqual("a", "b")).toBe(false);
|
|
12
|
+
expect(isContentEqual(null, void 0)).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
it("treats arrays by length + per-element strict-eq", function () {
|
|
15
|
+
expect(isContentEqual([], [])).toBe(true);
|
|
16
|
+
expect(isContentEqual([1, 2, 3], [1, 2, 3])).toBe(true);
|
|
17
|
+
expect(isContentEqual([1, 2, 3], [1, 2])).toBe(false);
|
|
18
|
+
expect(isContentEqual([1, 2, 3], [1, 2, 4])).toBe(false);
|
|
19
|
+
expect(isContentEqual([{
|
|
20
|
+
id: 1
|
|
21
|
+
}], [{
|
|
22
|
+
id: 1
|
|
23
|
+
}])).toBe(false);
|
|
24
|
+
var row = {
|
|
25
|
+
id: 1
|
|
26
|
+
};
|
|
27
|
+
expect(isContentEqual([row], [row])).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
it("treats objects by same keys + per-value strict-eq", function () {
|
|
30
|
+
expect(isContentEqual({}, {})).toBe(true);
|
|
31
|
+
expect(isContentEqual({
|
|
32
|
+
a: 1,
|
|
33
|
+
b: 2
|
|
34
|
+
}, {
|
|
35
|
+
a: 1,
|
|
36
|
+
b: 2
|
|
37
|
+
})).toBe(true);
|
|
38
|
+
expect(isContentEqual({
|
|
39
|
+
a: 1
|
|
40
|
+
}, {
|
|
41
|
+
a: 1,
|
|
42
|
+
b: 2
|
|
43
|
+
})).toBe(false);
|
|
44
|
+
expect(isContentEqual({
|
|
45
|
+
a: 1
|
|
46
|
+
}, {
|
|
47
|
+
a: 2
|
|
48
|
+
})).toBe(false);
|
|
49
|
+
expect(isContentEqual({
|
|
50
|
+
row: {
|
|
51
|
+
id: 1
|
|
52
|
+
}
|
|
53
|
+
}, {
|
|
54
|
+
row: {
|
|
55
|
+
id: 1
|
|
56
|
+
}
|
|
57
|
+
})).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it("rejects mismatched array vs object", function () {
|
|
60
|
+
expect(isContentEqual([], {})).toBe(false);
|
|
61
|
+
expect(isContentEqual([1], {
|
|
62
|
+
0: 1
|
|
63
|
+
})).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it("handles a typical zero singular-query refresh \u2014 same row content", function () {
|
|
66
|
+
var prev = {
|
|
67
|
+
id: "s1",
|
|
68
|
+
name: "warden",
|
|
69
|
+
level: 1,
|
|
70
|
+
hp: 60,
|
|
71
|
+
maxHp: 60
|
|
72
|
+
};
|
|
73
|
+
var next = {
|
|
74
|
+
id: "s1",
|
|
75
|
+
name: "warden",
|
|
76
|
+
level: 1,
|
|
77
|
+
hp: 60,
|
|
78
|
+
maxHp: 60
|
|
79
|
+
};
|
|
80
|
+
expect(isContentEqual(prev, next)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
it("detects a real refresh \u2014 one field changed", function () {
|
|
83
|
+
var prev = {
|
|
84
|
+
id: "s1",
|
|
85
|
+
hp: 60
|
|
86
|
+
};
|
|
87
|
+
var next = {
|
|
88
|
+
id: "s1",
|
|
89
|
+
hp: 59
|
|
90
|
+
};
|
|
91
|
+
expect(isContentEqual(prev, next)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
it('handles zero info objects ({type: "complete"} vs {type: "unknown"})', function () {
|
|
94
|
+
expect(isContentEqual({
|
|
95
|
+
type: "complete"
|
|
96
|
+
}, {
|
|
97
|
+
type: "complete"
|
|
98
|
+
})).toBe(true);
|
|
99
|
+
expect(isContentEqual({
|
|
100
|
+
type: "complete"
|
|
101
|
+
}, {
|
|
102
|
+
type: "unknown"
|
|
103
|
+
})).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
//# sourceMappingURL=useContentStable.test.native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["describe","expect","it","isContentEqual","toBe","id"],"sources":["../../src/useContentStable.test.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,QAAA,EAAUC,MAAA,EAAQC,EAAA,QAAU;AACrC,SAASC,cAAA,QAAsB;AAE/BH,QAAA,CAAS,kBAAkB,YAAM;EAC/BE,EAAA,CAAG,8BAA8B,YAAM;IACrCD,MAAA,CAAOE,cAAA,CAAe,GAAG,CAAC,CAAC,EAAEC,IAAA,CAAK,IAAI;IACtCH,MAAA,CAAOE,cAAA,CAAe,KAAK,GAAG,CAAC,EAAEC,IAAA,CAAK,IAAI;IAC1CH,MAAA,CAAOE,cAAA,CAAe,MAAM,IAAI,CAAC,EAAEC,IAAA,CAAK,IAAI;IAC5CH,MAAA,CAAOE,cAAA,CAAe,QAAW,MAAS,CAAC,EAAEC,IAAA,CAAK,IAAI;IACtDH,MAAA,CAAOE,cAAA,CAAe,MAAM,IAAI,CAAC,EAAEC,IAAA,CAAK,IAAI;IAC5CH,MAAA,CAAOE,cAAA,CAAe,GAAG,CAAC,CAAC,EAAEC,IAAA,CAAK,KAAK;IACvCH,MAAA,CAAOE,cAAA,CAAe,KAAK,GAAG,CAAC,EAAEC,IAAA,CAAK,KAAK;IAC3CH,MAAA,CAAOE,cAAA,CAAe,MAAM,MAAS,CAAC,EAAEC,IAAA,CAAK,KAAK;EACpD,CAAC;EAEDF,EAAA,CAAG,mDAAmD,YAAM;IAC1DD,MAAA,CAAOE,cAAA,CAAe,EAAC,EAAG,EAAE,CAAC,EAAEC,IAAA,CAAK,IAAI;IACxCH,MAAA,CAAOE,cAAA,CAAe,CACtB,GACA,GAEA,EAEA,GACA,GACD,GAEE,EACD,GAAAC,IAAO;IACPH,MAAA,CAAOE,cAAA,CAAe,CACtB,GACA,GAEA,EACD,GAEE,GACD,EACA,GAAAC,IAAO;IACRH,MAAA,CAAAE,cAAA,EAEE,GAID,GACA,EACA,GACD,GAEE,GACD,EACA,GAAAC,IAAM,MAAO,CAAE;IACfH,MAAA,CAAOE,cAAA,CAAe,CACvB;MAEEE,EAAA;IACD,EACA,GACD;MACFA,EAAA","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "on-zero",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.50",
|
|
4
4
|
"description": "A typed layer over @rocicorp/zero with queries, mutations, and permissions",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"source": "src/index.ts",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@take-out/helpers": "0.4.
|
|
86
|
+
"@take-out/helpers": "0.4.50",
|
|
87
87
|
"chokidar": "^4.0.3",
|
|
88
88
|
"citty": "^0.1.6",
|
|
89
89
|
"valibot": "^1.1.0"
|
package/src/createUseQuery.tsx
CHANGED
|
@@ -4,12 +4,14 @@ import { useContext, useMemo, useRef, type Context } from 'react'
|
|
|
4
4
|
|
|
5
5
|
import { useZeroDebug } from './helpers/useZeroDebug'
|
|
6
6
|
import { resolveQuery, type PlainQueryFn } from './resolveQuery'
|
|
7
|
+
import { useContentStable } from './useContentStable'
|
|
7
8
|
|
|
8
9
|
import type {
|
|
9
10
|
AnyQueryRegistry,
|
|
10
11
|
HumanReadable,
|
|
11
12
|
Query,
|
|
12
13
|
Schema as ZeroSchema,
|
|
14
|
+
TTL,
|
|
13
15
|
} from '@rocicorp/zero'
|
|
14
16
|
|
|
15
17
|
// false = enabled, 'empty' = disabled (return empty), 'last-value' = disabled (return cached)
|
|
@@ -17,7 +19,7 @@ export type QueryControlMode = false | 'empty' | 'last-value'
|
|
|
17
19
|
|
|
18
20
|
export type UseQueryOptions = {
|
|
19
21
|
enabled?: boolean | undefined
|
|
20
|
-
ttl?:
|
|
22
|
+
ttl?: TTL | undefined
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
type QueryResultDetails = ReturnType<typeof zeroUseQuery>[1]
|
|
@@ -114,12 +116,30 @@ export function createUseQuery<Schema extends ZeroSchema>({
|
|
|
114
116
|
const lastRef = useRef<any>(EMPTY_RESPONSE_PLURAL)
|
|
115
117
|
const [fn, paramsOrOptions, optionsArg] = args
|
|
116
118
|
|
|
117
|
-
const {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
const { params, options } = parseUseQueryArgs(paramsOrOptions, optionsArg)
|
|
120
|
+
|
|
121
|
+
// value-keyed memoization. callers conventionally pass inline objects to
|
|
122
|
+
// useQuery (`useQuery(byId, {id})`), so a raw identity dep would bust
|
|
123
|
+
// resolveQuery and re-create the query every render. JSON is faster than
|
|
124
|
+
// generic deep-equal for the small, simple-valued objects zero calls
|
|
125
|
+
// actually take, and matches what createUseQueryDirect does.
|
|
126
|
+
const paramsKey = params === undefined ? '' : JSON.stringify(params)
|
|
127
|
+
const queryRequest = useMemo(
|
|
128
|
+
() => resolveQuery({ customQueries, fn, params }),
|
|
129
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
130
|
+
[fn, paramsKey],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// extract option values as primitives — a fresh options object each render
|
|
134
|
+
// shouldn't propagate as a new ref into zero-react.
|
|
135
|
+
let optionEnabled: boolean | undefined
|
|
136
|
+
let optionTTL: TTL | undefined
|
|
137
|
+
if (typeof options === 'boolean') {
|
|
138
|
+
optionEnabled = options
|
|
139
|
+
} else if (options) {
|
|
140
|
+
optionEnabled = options.enabled
|
|
141
|
+
optionTTL = options.ttl
|
|
142
|
+
}
|
|
123
143
|
|
|
124
144
|
// when disabled, force enabled=false through to zero's useQuery so its
|
|
125
145
|
// viewStore returns a disabled view (no zero.clientID read, no view
|
|
@@ -127,11 +147,11 @@ export function createUseQuery<Schema extends ZeroSchema>({
|
|
|
127
147
|
// we still call zeroUseQuery unconditionally so hook order stays stable
|
|
128
148
|
// across DisabledContext changes — the disable check after only affects
|
|
129
149
|
// the return value.
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
150
|
+
const effectiveEnabled = disableMode ? false : optionEnabled
|
|
151
|
+
const effectiveOptions = useMemo(
|
|
152
|
+
() => ({ enabled: effectiveEnabled, ttl: optionTTL }),
|
|
153
|
+
[effectiveEnabled, optionTTL],
|
|
154
|
+
)
|
|
135
155
|
const rawOut = zeroUseQuery(queryRequest, effectiveOptions)
|
|
136
156
|
|
|
137
157
|
// normalize the rare null-data first snapshot for plural queries to []:
|
|
@@ -149,6 +169,18 @@ export function createUseQuery<Schema extends ZeroSchema>({
|
|
|
149
169
|
}
|
|
150
170
|
}
|
|
151
171
|
|
|
172
|
+
// content-stable inner refs. zero-react `#onData` does deepClone(snap) on
|
|
173
|
+
// every view notification, so even a no-op re-emit produces fresh data
|
|
174
|
+
// identity. consumers that destructure `[row] = useQuery(byId, {id})` and
|
|
175
|
+
// depend on `row` see a re-fire on each notification even when the row's
|
|
176
|
+
// content hasn't changed. anchor `data` + `info` to the prior reference
|
|
177
|
+
// when shallow-content matches so identity-based deps stay quiet.
|
|
178
|
+
const stableData = useContentStable(out?.[0])
|
|
179
|
+
const stableInfo = useContentStable(out?.[1])
|
|
180
|
+
if (stableData !== out?.[0] || stableInfo !== out?.[1]) {
|
|
181
|
+
out = [stableData, stableInfo]
|
|
182
|
+
}
|
|
183
|
+
|
|
152
184
|
if (process.env.NODE_ENV === 'development') {
|
|
153
185
|
if (process.env.DEBUG_ZERO_QUERIES === '1')
|
|
154
186
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type UseQueryOptions,
|
|
16
16
|
} from './createUseQuery'
|
|
17
17
|
import { resolveQuery } from './resolveQuery'
|
|
18
|
+
import { useContentStable } from './useContentStable'
|
|
18
19
|
|
|
19
20
|
import type {
|
|
20
21
|
AnyQueryRegistry,
|
|
@@ -327,12 +328,20 @@ export function createUseQueryDirect<Schema extends ZeroSchema>({
|
|
|
327
328
|
}, [queryRequest, enabled, ttl, version])
|
|
328
329
|
|
|
329
330
|
const getEmpty = () => emptyForQuery as DirectSnapshot
|
|
330
|
-
const
|
|
331
|
+
const rawOut = useSyncExternalStore(
|
|
331
332
|
view ? view.subscribe : DISABLED_SUBSCRIBE,
|
|
332
333
|
view ? view.getSnapshot : getEmpty,
|
|
333
334
|
view ? view.getSnapshot : getEmpty,
|
|
334
335
|
)
|
|
335
336
|
|
|
337
|
+
// content-stable inner refs (see createUseQuery.tsx for the rationale).
|
|
338
|
+
const stableData = useContentStable(rawOut[0])
|
|
339
|
+
const stableInfo = useContentStable(rawOut[1])
|
|
340
|
+
const out: DirectSnapshot =
|
|
341
|
+
stableData !== rawOut[0] || stableInfo !== rawOut[1]
|
|
342
|
+
? ([stableData, stableInfo] as DirectSnapshot)
|
|
343
|
+
: rawOut
|
|
344
|
+
|
|
336
345
|
if (!disableMode) {
|
|
337
346
|
lastRef.current = out
|
|
338
347
|
return out
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { isContentEqual } from './useContentStable'
|
|
3
|
+
|
|
4
|
+
describe('isContentEqual', () => {
|
|
5
|
+
it('treats primitives by value', () => {
|
|
6
|
+
expect(isContentEqual(1, 1)).toBe(true)
|
|
7
|
+
expect(isContentEqual('a', 'a')).toBe(true)
|
|
8
|
+
expect(isContentEqual(true, true)).toBe(true)
|
|
9
|
+
expect(isContentEqual(undefined, undefined)).toBe(true)
|
|
10
|
+
expect(isContentEqual(null, null)).toBe(true)
|
|
11
|
+
expect(isContentEqual(1, 2)).toBe(false)
|
|
12
|
+
expect(isContentEqual('a', 'b')).toBe(false)
|
|
13
|
+
expect(isContentEqual(null, undefined)).toBe(false)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('treats arrays by length + per-element strict-eq', () => {
|
|
17
|
+
expect(isContentEqual([], [])).toBe(true)
|
|
18
|
+
expect(isContentEqual([1, 2, 3], [1, 2, 3])).toBe(true)
|
|
19
|
+
expect(isContentEqual([1, 2, 3], [1, 2])).toBe(false)
|
|
20
|
+
expect(isContentEqual([1, 2, 3], [1, 2, 4])).toBe(false)
|
|
21
|
+
// distinct objects with same fields → strict-eq fails per element
|
|
22
|
+
expect(isContentEqual([{ id: 1 }], [{ id: 1 }])).toBe(false)
|
|
23
|
+
// same object ref shared → strict-eq holds
|
|
24
|
+
const row = { id: 1 }
|
|
25
|
+
expect(isContentEqual([row], [row])).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('treats objects by same keys + per-value strict-eq', () => {
|
|
29
|
+
expect(isContentEqual({}, {})).toBe(true)
|
|
30
|
+
expect(isContentEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true)
|
|
31
|
+
expect(isContentEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false)
|
|
32
|
+
expect(isContentEqual({ a: 1 }, { a: 2 })).toBe(false)
|
|
33
|
+
// distinct nested objects → strict-eq fails on the nested value
|
|
34
|
+
expect(isContentEqual({ row: { id: 1 } }, { row: { id: 1 } })).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('rejects mismatched array vs object', () => {
|
|
38
|
+
expect(isContentEqual([], {})).toBe(false)
|
|
39
|
+
expect(isContentEqual([1], { 0: 1 })).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('handles a typical zero singular-query refresh — same row content', () => {
|
|
43
|
+
// simulating zero's deepClone: the row is a fresh object but every field
|
|
44
|
+
// matches the previous snapshot. shallow strict-eq sees fresh nested but
|
|
45
|
+
// top-level keys are primitives → equal.
|
|
46
|
+
const prev = { id: 's1', name: 'warden', level: 1, hp: 60, maxHp: 60 }
|
|
47
|
+
const next = { id: 's1', name: 'warden', level: 1, hp: 60, maxHp: 60 }
|
|
48
|
+
expect(isContentEqual(prev, next)).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('detects a real refresh — one field changed', () => {
|
|
52
|
+
const prev = { id: 's1', hp: 60 }
|
|
53
|
+
const next = { id: 's1', hp: 59 }
|
|
54
|
+
expect(isContentEqual(prev, next)).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('handles zero info objects ({type: "complete"} vs {type: "unknown"})', () => {
|
|
58
|
+
expect(isContentEqual({ type: 'complete' }, { type: 'complete' })).toBe(true)
|
|
59
|
+
expect(isContentEqual({ type: 'complete' }, { type: 'unknown' })).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// content-stable identity for useQuery's inner data + info values.
|
|
2
|
+
//
|
|
3
|
+
// upstream zero-react's view does `data = deepClone(snap)` inside #onData
|
|
4
|
+
// whenever the materialized view notifies, which means each notification
|
|
5
|
+
// produces a fresh JS reference even when the row's content hasn't changed.
|
|
6
|
+
// consumers that destructure `[row] = useQuery(byId, {id})` and then use
|
|
7
|
+
// `row` as a useEffect dep — or that depend on `[rows]` for a list query —
|
|
8
|
+
// see a "change" on every notification and re-run effects redundantly.
|
|
9
|
+
//
|
|
10
|
+
// `useContentStable(value)` keeps a ref to the previous value and returns the
|
|
11
|
+
// old reference when shallow-content equality holds, so downstream identity
|
|
12
|
+
// comparisons (`useMemo`, `useEffect`, `useCallback`) don't false-fire.
|
|
13
|
+
//
|
|
14
|
+
// equality semantics:
|
|
15
|
+
// - primitives + same-reference → return as-is
|
|
16
|
+
// - arrays: same length + each element strict-eq → reuse previous ref
|
|
17
|
+
// - objects: same keys + each value strict-eq → reuse previous ref
|
|
18
|
+
// - mismatches → swap the ref forward
|
|
19
|
+
//
|
|
20
|
+
// the per-element check is strict-eq (not recursive). that's intentional —
|
|
21
|
+
// it catches the common case where a query result is the same row(s) on a
|
|
22
|
+
// re-emit. for nested-object stability the rows themselves get the same
|
|
23
|
+
// treatment one level up (the wrapping useQuery already applies this to
|
|
24
|
+
// `data` so an array-of-rows query holds row references stable if each row
|
|
25
|
+
// content-matches its previous snapshot row).
|
|
26
|
+
|
|
27
|
+
import { useRef } from 'react'
|
|
28
|
+
|
|
29
|
+
function arraysShallowEqual(a: readonly unknown[], b: readonly unknown[]): boolean {
|
|
30
|
+
if (a === b) return true
|
|
31
|
+
if (a.length !== b.length) return false
|
|
32
|
+
for (let i = 0; i < a.length; i++) {
|
|
33
|
+
if (!Object.is(a[i], b[i])) return false
|
|
34
|
+
}
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function objectsShallowEqual(a: object, b: object): boolean {
|
|
39
|
+
if (a === b) return true
|
|
40
|
+
const ak = Object.keys(a as Record<string, unknown>)
|
|
41
|
+
const bk = Object.keys(b as Record<string, unknown>)
|
|
42
|
+
if (ak.length !== bk.length) return false
|
|
43
|
+
for (const k of ak) {
|
|
44
|
+
if (
|
|
45
|
+
!Object.is(
|
|
46
|
+
(a as Record<string, unknown>)[k],
|
|
47
|
+
(b as Record<string, unknown>)[k],
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
return true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isContentEqual(a: unknown, b: unknown): boolean {
|
|
56
|
+
if (Object.is(a, b)) return true
|
|
57
|
+
if (a === null || b === null || a === undefined || b === undefined) return false
|
|
58
|
+
if (typeof a !== 'object' || typeof b !== 'object') return false
|
|
59
|
+
const aIsArr = Array.isArray(a)
|
|
60
|
+
const bIsArr = Array.isArray(b)
|
|
61
|
+
if (aIsArr !== bIsArr) return false
|
|
62
|
+
if (aIsArr && bIsArr) {
|
|
63
|
+
return arraysShallowEqual(a as readonly unknown[], b as readonly unknown[])
|
|
64
|
+
}
|
|
65
|
+
return objectsShallowEqual(a as object, b as object)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// returns `value` on first render, then returns the previous reference for as
|
|
69
|
+
// long as content-equality holds. mutating the ref during render is safe here
|
|
70
|
+
// because we read its current value AFTER deciding whether to swap — no read-
|
|
71
|
+
// then-write race that React would warn about.
|
|
72
|
+
export function useContentStable<T>(value: T): T {
|
|
73
|
+
const ref = useRef<T>(value)
|
|
74
|
+
if (!isContentEqual(ref.current, value)) {
|
|
75
|
+
ref.current = value
|
|
76
|
+
}
|
|
77
|
+
return ref.current
|
|
78
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useQuery as zeroUseQuery } from '@rocicorp/zero/react';
|
|
2
2
|
import { type Context } from 'react';
|
|
3
3
|
import { type PlainQueryFn } from './resolveQuery';
|
|
4
|
-
import type { AnyQueryRegistry, HumanReadable, Query, Schema as ZeroSchema } from '@rocicorp/zero';
|
|
4
|
+
import type { AnyQueryRegistry, HumanReadable, Query, Schema as ZeroSchema, TTL } from '@rocicorp/zero';
|
|
5
5
|
export type QueryControlMode = false | 'empty' | 'last-value';
|
|
6
6
|
export type UseQueryOptions = {
|
|
7
7
|
enabled?: boolean | undefined;
|
|
8
|
-
ttl?:
|
|
8
|
+
ttl?: TTL | undefined;
|
|
9
9
|
};
|
|
10
10
|
type QueryResultDetails = ReturnType<typeof zeroUseQuery>[1];
|
|
11
11
|
export type QueryResult<TReturn> = readonly [HumanReadable<TReturn>, QueryResultDetails];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createUseQuery.d.ts","sourceRoot":"","sources":["../src/createUseQuery.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAE/D,OAAO,EAA+B,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAGjE,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"createUseQuery.d.ts","sourceRoot":"","sources":["../src/createUseQuery.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAE/D,OAAO,EAA+B,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAGjE,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAGhE,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,KAAK,EACL,MAAM,IAAI,UAAU,EACpB,GAAG,EACJ,MAAM,gBAAgB,CAAA;AAGvB,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,YAAY,CAAA;AAE7D,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,GAAG,CAAC,EAAE,GAAG,GAAG,SAAS,CAAA;CACtB,CAAA;AAED,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;AAC5D,MAAM,MAAM,WAAW,CAAC,OAAO,IAAI,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,CAAA;AAExF,YAAY,EAAE,YAAY,EAAE,CAAA;AAE5B,MAAM,MAAM,YAAY,CAAC,MAAM,SAAS,UAAU,IAAI;IAEpD,CAAC,IAAI,EAAE,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EAC5D,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,MAAM,EAAE,IAAI,EACZ,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAC;IAGxB,CAAC,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EACtD,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAA;CACxB,CAAA;AAiBD,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,OAAO,GAAG,SAAS,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAU5F;AAGD,wBAAgB,iBAAiB,CAAC,eAAe,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG;;;EAYtE;AAED,wBAAgB,cAAc,CAAC,MAAM,SAAS,UAAU,EAAE,EACxD,eAAe,EACf,aAAa,GACd,EAAE;IACD,eAAe,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC1C,aAAa,EAAE,gBAAgB,CAAA;CAChC,GAAG,YAAY,CAAC,MAAM,CAAC,CAoHvB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createUseQueryDirect.d.ts","sourceRoot":"","sources":["../src/createUseQueryDirect.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAqD,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAEvF,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAElB,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"createUseQueryDirect.d.ts","sourceRoot":"","sources":["../src/createUseQueryDirect.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAmB,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAqD,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAEvF,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAElB,MAAM,kBAAkB,CAAA;AAIzB,OAAO,KAAK,EACV,gBAAgB,EAEhB,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAkBvB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CACT,KAAK,EAAE,GAAG,EACV,OAAO,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,GAAG,CAAA;KAAE,GACtB;QACD,WAAW,CACT,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,gBAAgB,KAAK,IAAI,GACpE,IAAI,CAAA;QACP,OAAO,IAAI,IAAI,CAAA;QACf,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAAA;KAC1B,CAAA;CACF,CAAA;AAED,MAAM,MAAM,oBAAoB,CAAC,MAAM,SAAS,UAAU,IAAI,CAAC,KAAK,EAAE;IACpE,eAAe,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAC1C,aAAa,EAAE,gBAAgB,CAAA;IAC/B,OAAO,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAA;IACxC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CAC7B,KAAK,YAAY,CAAC,MAAM,CAAC,CAAA;AAG1B,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,KAAK,GAAG,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AA2LD,wBAAgB,oBAAoB,CAAC,MAAM,SAAS,UAAU,EAAE,EAC9D,eAAe,EACf,aAAa,EACb,OAAO,EACP,WAAW,GACZ,EAAE,UAAU,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,CAiGpE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useContentStable.d.ts","sourceRoot":"","sources":["../src/useContentStable.ts"],"names":[],"mappings":"AAsDA,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAW9D;AAMD,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAM/C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useContentStable.test.d.ts","sourceRoot":"","sources":["../src/useContentStable.test.ts"],"names":[],"mappings":""}
|