@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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # @effect-app/vue
2
2
 
3
+ ## 4.0.0-beta.289
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [a1b59bc]
8
+ - effect-app@4.0.0-beta.289
9
+
3
10
  ## 4.0.0-beta.288
4
11
 
5
12
  ### Minor Changes
@@ -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.288",
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.288"
13
+ "effect-app": "4.0.0-beta.289"
14
14
  },
15
15
  "peerDependencies": {
16
16
  "@effect/atom-vue": "^4.0.0-beta.90",