@effect-app/vue 4.0.0-beta.288 → 4.0.0-beta.289
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/CHANGELOG.md +7 -0
- package/docs/query-key-invalidation.md +106 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Query-key invalidation semantics
|
|
2
|
+
|
|
3
|
+
How the Atom query engine reproduces TanStack Query's **hierarchical (namespace) invalidation**, and where it intentionally differs.
|
|
4
|
+
|
|
5
|
+
## The behavior we must match
|
|
6
|
+
|
|
7
|
+
TanStack `queryClient.invalidateQueries({ queryKey: K })` defaults to `exact: false`:
|
|
8
|
+
**K matches every query whose key has K as a prefix.**
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
invalidate ["A","B"] -> matches ["A","B"], ["A","B","C"], ["A","B",input], ...
|
|
12
|
+
invalidate ["A","B","C"] -> does NOT match ["A","B"] (K longer than the key)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Two requirements: don't invalidate **more** than TanStack would, and don't invalidate **less**.
|
|
16
|
+
|
|
17
|
+
## How the Atom engine does it
|
|
18
|
+
|
|
19
|
+
TanStack stores each full key and runs a prefix compare at invalidation time. The Atom
|
|
20
|
+
engine inverts that: each query **pre-registers all prefixes of its full key**, and
|
|
21
|
+
invalidation does an exact structural-hash lookup.
|
|
22
|
+
|
|
23
|
+
- Full key: `fullKey = [...baseKey, input]` where `baseKey = makeQueryKey(self)` is the
|
|
24
|
+
hierarchical, input-independent namespace (e.g. `["$Project","$Configuration","get"]`).
|
|
25
|
+
- On mount, the query atom registers `prefixesOf(fullKey)` — every non-empty prefix:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
["$Project","$Configuration","get", input]
|
|
29
|
+
registers:
|
|
30
|
+
["$Project"]
|
|
31
|
+
["$Project","$Configuration"]
|
|
32
|
+
["$Project","$Configuration","get"]
|
|
33
|
+
["$Project","$Configuration","get", input]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
See `buildQueryFamily` → `prefixesOf` → `factory.withReactivity` in
|
|
37
|
+
[`src/atomQuery.ts`](../src/atomQuery.ts) (registration around L416-424).
|
|
38
|
+
- Invalidation hashes K and fires the handlers registered under that exact hash
|
|
39
|
+
(`Reactivity.invalidate` → `keysToHashes` in
|
|
40
|
+
`repos/effect/packages/effect/src/unstable/reactivity/Reactivity.ts`).
|
|
41
|
+
|
|
42
|
+
### Why this equals TanStack's match set
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
K matches a query
|
|
46
|
+
<=> K equals one of that query's registered prefixes
|
|
47
|
+
<=> K is a prefix of the query's full key
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That is exactly TanStack's `exact:false` rule.
|
|
51
|
+
|
|
52
|
+
- **Not too much** — `invalidate ["A","B"]` hits only queries whose full key starts with
|
|
53
|
+
`["A","B"]`. An over-specific K (longer than a query's key) matches nothing in both
|
|
54
|
+
systems, because a query never registers a prefix longer than itself.
|
|
55
|
+
- **Not too little** — a short namespace `["A"]` reaches every query under it, because
|
|
56
|
+
each registered the `["A"]` prefix. The mutation default `getQueryKey`
|
|
57
|
+
(e.g. `["$Project"]`, `defaultGetQueryKey` in [`src/mutate.ts`](../src/mutate.ts)) is
|
|
58
|
+
always a leading prefix of `baseKey`, so default namespace invalidation reaches all
|
|
59
|
+
inputs and sub-resources.
|
|
60
|
+
|
|
61
|
+
## Why multiple keys must be registered
|
|
62
|
+
|
|
63
|
+
This is the crux. A single exact-key registration would only fire when invalidation used
|
|
64
|
+
that exact key — losing TanStack's "parent namespace invalidates all children" behavior.
|
|
65
|
+
Registering **every prefix** is what restores hierarchical reach under an exact-lookup
|
|
66
|
+
mechanism. Drop a prefix and that namespace level stops invalidating its children
|
|
67
|
+
(invalidates too little); register a key that isn't a prefix and it invalidates unrelated
|
|
68
|
+
queries (too much).
|
|
69
|
+
|
|
70
|
+
## Hashing consistency (register ↔ invalidate)
|
|
71
|
+
|
|
72
|
+
Both sides route through `keysToHashes`, which hashes **each element** of the keys array:
|
|
73
|
+
|
|
74
|
+
- Registration: `withReactivity(reactivityKeys)` → `registerUnsafe` — `reactivityKeys` is a
|
|
75
|
+
list of prefix-arrays, each hashed via `Hash.hash`.
|
|
76
|
+
- Invalidation: mutations pass `ReadonlyArray<ReadonlyArray<unknown>>` — a _list of
|
|
77
|
+
key-arrays_ (`buildInvalidateCache` in [`src/mutate.ts`](../src/mutate.ts)) — so each
|
|
78
|
+
element is a full namespaced array, matching the registration shape.
|
|
79
|
+
|
|
80
|
+
The await-tracking map (`keyAtoms` in `atomQuery.ts`) keys on the same `Hash.hash(key)`, so
|
|
81
|
+
`invalidateAndAwait` resolves once exactly the matched live queries have settled.
|
|
82
|
+
|
|
83
|
+
## Intentional differences / caveats
|
|
84
|
+
|
|
85
|
+
1. **Hash-only matching, no equality fallback.** TanStack compares keys with
|
|
86
|
+
`partialDeepEqual`. The Atom path keys its handler map purely on `Hash.hash` with no
|
|
87
|
+
tiebreak. A hash collision between two distinct keys would cross-invalidate
|
|
88
|
+
(over-invalidate). Probability is low (array hashes combine element hashes) but it is
|
|
89
|
+
the one real semantic divergence from TanStack's exact compare. Same applies to
|
|
90
|
+
`keyAtoms` and `uniqueKeys`.
|
|
91
|
+
2. **Input is one opaque trailing element** (`[...baseKey, input]`). You can invalidate at
|
|
92
|
+
any namespace granularity but not _within_ an input (no "all inputs where
|
|
93
|
+
status=active"). TanStack with the same key shape behaves identically — parity, just a
|
|
94
|
+
granularity ceiling.
|
|
95
|
+
3. **O(depth) registrations per live query** (depth+1 prefixes, in both the reactivity
|
|
96
|
+
handler map and `keyAtoms`). Negligible at typical depths of 2-4.
|
|
97
|
+
4. **Reaches only "alive" atoms** (mounted, or cached within `idleTTL`). `setIdleTTL` is
|
|
98
|
+
applied last in the atom chain so cached-but-unmounted queries stay registered and are
|
|
99
|
+
still hit; a query GC'd past idle TTL is gone and refetches fresh on next mount anyway —
|
|
100
|
+
matching TanStack `gcTime` semantics.
|
|
101
|
+
|
|
102
|
+
## Verdict
|
|
103
|
+
|
|
104
|
+
Sound. The prefix-registration faithfully reproduces TanStack's hierarchical invalidation
|
|
105
|
+
in both directions (not too much, not too little). The only behavioral gap is the
|
|
106
|
+
theoretical hash-collision case in caveat 1.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-app/vue",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.289",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/effect-ts-app/libs/tree/main/packages/vue",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"@vueuse/core": "^14.3.0",
|
|
11
11
|
"change-case": "^5.4.4",
|
|
12
12
|
"query-string": "^9.4.0",
|
|
13
|
-
"effect-app": "4.0.0-beta.
|
|
13
|
+
"effect-app": "4.0.0-beta.289"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"@effect/atom-vue": "^4.0.0-beta.90",
|