fetchium 0.1.0 → 0.2.0
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 +12 -5
- package/README.md +1 -1
- package/dist/cjs/development/QueryAdapter-DUo338ga.js +2 -0
- package/dist/cjs/development/QueryAdapter-DUo338ga.js.map +1 -0
- package/dist/cjs/development/QueryClient-m7BzCIe9.js +2 -0
- package/dist/cjs/development/QueryClient-m7BzCIe9.js.map +1 -0
- package/dist/cjs/development/index.js +1 -1
- package/dist/cjs/development/index.js.map +1 -1
- package/dist/cjs/development/mutation-wUhcGxKl.js +2 -0
- package/dist/cjs/development/mutation-wUhcGxKl.js.map +1 -0
- package/dist/cjs/development/react/index.js +1 -1
- package/dist/cjs/development/rest/index.js +2 -0
- package/dist/cjs/development/rest/index.js.map +1 -0
- package/dist/cjs/development/topic/index.js +2 -0
- package/dist/cjs/development/topic/index.js.map +1 -0
- package/dist/cjs/production/QueryAdapter-DUo338ga.js +2 -0
- package/dist/cjs/production/QueryAdapter-DUo338ga.js.map +1 -0
- package/dist/cjs/production/QueryClient-4T90peFN.js +2 -0
- package/dist/cjs/production/QueryClient-4T90peFN.js.map +1 -0
- package/dist/cjs/production/index.js +1 -1
- package/dist/cjs/production/index.js.map +1 -1
- package/dist/cjs/production/mutation-Dk0gznwX.js +2 -0
- package/dist/cjs/production/mutation-Dk0gznwX.js.map +1 -0
- package/dist/cjs/production/react/index.js +1 -1
- package/dist/cjs/production/rest/index.js +2 -0
- package/dist/cjs/production/rest/index.js.map +1 -0
- package/dist/cjs/production/topic/index.js +2 -0
- package/dist/cjs/production/topic/index.js.map +1 -0
- package/dist/esm/MutationResult.d.ts +0 -1
- package/dist/esm/MutationResult.d.ts.map +1 -1
- package/dist/esm/QueryAdapter.d.ts +49 -0
- package/dist/esm/QueryAdapter.d.ts.map +1 -0
- package/dist/esm/QueryClient.d.ts +26 -4
- package/dist/esm/QueryClient.d.ts.map +1 -1
- package/dist/esm/QueryResult.d.ts +11 -11
- package/dist/esm/QueryResult.d.ts.map +1 -1
- package/dist/esm/development/QueryAdapter-Bu5UJjE4.js +14 -0
- package/dist/esm/development/QueryAdapter-Bu5UJjE4.js.map +1 -0
- package/dist/esm/development/QueryClient-BajBmpnA.js +2572 -0
- package/dist/esm/development/QueryClient-BajBmpnA.js.map +1 -0
- package/dist/esm/development/index.js +29 -100
- package/dist/esm/development/index.js.map +1 -1
- package/dist/esm/development/mutation-DAOZE4Ok.js +58 -0
- package/dist/esm/development/mutation-DAOZE4Ok.js.map +1 -0
- package/dist/esm/development/react/index.js +1 -1
- package/dist/esm/development/rest/index.js +142 -0
- package/dist/esm/development/rest/index.js.map +1 -0
- package/dist/esm/development/{shared-Dq2yW78d.js → shared-DcuVH8Pf.js} +5 -5
- package/dist/esm/development/{shared-Dq2yW78d.js.map → shared-DcuVH8Pf.js.map} +1 -1
- package/dist/esm/development/stores/async.js +6 -6
- package/dist/esm/development/stores/sync.js +5 -5
- package/dist/esm/development/topic/index.js +86 -0
- package/dist/esm/development/topic/index.js.map +1 -0
- package/dist/esm/index.d.ts +5 -4
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/mutation.d.ts +6 -19
- package/dist/esm/mutation.d.ts.map +1 -1
- package/dist/esm/production/QueryAdapter-Bu5UJjE4.js +14 -0
- package/dist/esm/production/QueryAdapter-Bu5UJjE4.js.map +1 -0
- package/dist/esm/production/{QueryClient-BP0Z1rQV.js → QueryClient-KH0Ex_8m.js} +595 -591
- package/dist/esm/production/QueryClient-KH0Ex_8m.js.map +1 -0
- package/dist/esm/production/index.js +29 -100
- package/dist/esm/production/index.js.map +1 -1
- package/dist/esm/production/mutation-C7BOChR2.js +58 -0
- package/dist/esm/production/mutation-C7BOChR2.js.map +1 -0
- package/dist/esm/production/react/index.js +1 -1
- package/dist/esm/production/rest/index.js +142 -0
- package/dist/esm/production/rest/index.js.map +1 -0
- package/dist/esm/production/{shared-Dq2yW78d.js → shared-DcuVH8Pf.js} +5 -5
- package/dist/esm/production/{shared-Dq2yW78d.js.map → shared-DcuVH8Pf.js.map} +1 -1
- package/dist/esm/production/stores/async.js +6 -6
- package/dist/esm/production/stores/sync.js +5 -5
- package/dist/esm/production/topic/index.js +86 -0
- package/dist/esm/production/topic/index.js.map +1 -0
- package/dist/esm/query-types.d.ts +2 -4
- package/dist/esm/query-types.d.ts.map +1 -1
- package/dist/esm/query.d.ts +17 -39
- package/dist/esm/query.d.ts.map +1 -1
- package/dist/esm/rest/RESTMutation.d.ts +18 -0
- package/dist/esm/rest/RESTMutation.d.ts.map +1 -0
- package/dist/esm/rest/RESTQuery.d.ts +24 -0
- package/dist/esm/rest/RESTQuery.d.ts.map +1 -0
- package/dist/esm/rest/RESTQueryAdapter.d.ts +34 -0
- package/dist/esm/rest/RESTQueryAdapter.d.ts.map +1 -0
- package/dist/esm/rest/index.d.ts +5 -0
- package/dist/esm/rest/index.d.ts.map +1 -0
- package/dist/esm/stores/shared.d.ts.map +1 -1
- package/dist/esm/testing/MockClient.d.ts +64 -0
- package/dist/esm/testing/MockClient.d.ts.map +1 -0
- package/dist/esm/testing/auto-generate.d.ts +20 -0
- package/dist/esm/testing/auto-generate.d.ts.map +1 -0
- package/dist/esm/testing/entity-factory.d.ts +13 -0
- package/dist/esm/testing/entity-factory.d.ts.map +1 -0
- package/dist/esm/testing/index.d.ts +6 -0
- package/dist/esm/testing/index.d.ts.map +1 -0
- package/dist/esm/testing/types.d.ts +37 -0
- package/dist/esm/testing/types.d.ts.map +1 -0
- package/dist/esm/topic/TopicQuery.d.ts +10 -0
- package/dist/esm/topic/TopicQuery.d.ts.map +1 -0
- package/dist/esm/topic/TopicQueryAdapter.d.ts +43 -0
- package/dist/esm/topic/TopicQueryAdapter.d.ts.map +1 -0
- package/dist/esm/topic/index.d.ts +3 -0
- package/dist/esm/topic/index.d.ts.map +1 -0
- package/dist/esm/typeDefs.d.ts +1 -1
- package/dist/esm/types.d.ts +9 -4
- package/dist/esm/types.d.ts.map +1 -1
- package/package.json +51 -4
- package/plugin/.claude-plugin/plugin.json +10 -0
- package/plugin/agents/fetchium.md +168 -0
- package/plugin/docs/api/fetchium-react.md +135 -0
- package/plugin/docs/api/fetchium.md +674 -0
- package/plugin/docs/api/stores-async.md +219 -0
- package/plugin/docs/api/stores-sync.md +133 -0
- package/plugin/docs/core/entities.md +351 -0
- package/plugin/docs/core/queries.md +600 -0
- package/plugin/docs/core/streaming.md +550 -0
- package/plugin/docs/core/types.md +374 -0
- package/plugin/docs/data/caching.md +298 -0
- package/plugin/docs/data/live-data.md +435 -0
- package/plugin/docs/data/mutations.md +465 -0
- package/plugin/docs/guides/auth.md +318 -0
- package/plugin/docs/guides/error-handling.md +351 -0
- package/plugin/docs/guides/offline.md +270 -0
- package/plugin/docs/guides/testing.md +301 -0
- package/plugin/docs/quickstart.md +170 -0
- package/plugin/docs/reference/pagination.md +519 -0
- package/plugin/docs/reference/rest-queries.md +107 -0
- package/plugin/docs/reference/why-signalium.md +364 -0
- package/plugin/docs/setup/project-setup.md +319 -0
- package/plugin/install.mjs +88 -0
- package/plugin/skills/design/SKILL.md +140 -0
- package/plugin/skills/teach/SKILL.md +105 -0
- package/dist/cjs/development/QueryClient-CpmwggOn.js +0 -2
- package/dist/cjs/development/QueryClient-CpmwggOn.js.map +0 -1
- package/dist/cjs/production/QueryClient-qi3bR0eD.js +0 -2
- package/dist/cjs/production/QueryClient-qi3bR0eD.js.map +0 -1
- package/dist/esm/development/QueryClient-DRZtPKFD.js +0 -2568
- package/dist/esm/development/QueryClient-DRZtPKFD.js.map +0 -1
- package/dist/esm/production/QueryClient-BP0Z1rQV.js.map +0 -1
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Types
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Fetchium includes a DSL for defining the shape of your data - parameters, results, and entity fields. Fetchium uses these shapes to **parse API responses**, **normalize entities** into the cache, and **infer TypeScript types** so your queries are fully typed end-to-end.
|
|
6
|
+
|
|
7
|
+
This type validation system is focused on validating **JSON**. It is not a general-purpose validator, such as tools like [Zod](https://zod.dev/), which can be used to validate and parse nearly _any_ object that can be described with _TypeScript_. They support input values like _functions_, _custom classes_, objects with _circular references_, and so on.
|
|
8
|
+
|
|
9
|
+
By contrast, Fetchium's scope is _intentionally narrow_ and focused on _performance and evolution_. The DSL only includes the bare necessities to describe the _majority_ of JSON based APIs, with certain edge-case anti-patterns excluded entirely. It also includes an opinionated set of default behaviors designed to help you build robust APIs that can change over time.
|
|
10
|
+
|
|
11
|
+
## DSL Reference
|
|
12
|
+
|
|
13
|
+
The type DSL includes the usual suspects of such systems:
|
|
14
|
+
|
|
15
|
+
- Base primitives
|
|
16
|
+
- Objects/Arrays/Records
|
|
17
|
+
- Unions
|
|
18
|
+
- Helpers for optional/nullable/nullish union types
|
|
19
|
+
|
|
20
|
+
| Definition | TypeScript type |
|
|
21
|
+
| ------------------- | ------------------------ |
|
|
22
|
+
| `t.string` | `string` |
|
|
23
|
+
| `t.number` | `number` |
|
|
24
|
+
| `t.boolean` | `boolean` |
|
|
25
|
+
| `t.null` | `null` |
|
|
26
|
+
| `t.undefined` | `undefined` |
|
|
27
|
+
| `t.object({ ... })` | `{ ... }` |
|
|
28
|
+
| `t.array(type)` | `T[]` |
|
|
29
|
+
| `t.record(type)` | `Record<string, T>` |
|
|
30
|
+
| `t.union(...types)` | Union of types |
|
|
31
|
+
| `t.optional(type)` | `T \| undefined` |
|
|
32
|
+
| `t.nullable(type)` | `T \| null` |
|
|
33
|
+
| `t.nullish(type)` | `T \| undefined \| null` |
|
|
34
|
+
|
|
35
|
+
These can be combined in standard, predictable ways:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
t.union(t.string, t.number);
|
|
39
|
+
|
|
40
|
+
t.object({ foo: t.string, bar: t.number });
|
|
41
|
+
|
|
42
|
+
t.array(t.nullable(t.boolean));
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Notably there are no _chaining_ APIs, e.g. `t.string.optional`. While convenient and readable, chaining APIs add a lot of _weight_. Every primitive type becomes an object, and every combination of primitive types becomes a _new_ object. Under the hood, Fetchium represents primitive types as _number masks_, meaning that for a type definition like:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
t.object({
|
|
49
|
+
name: t.string,
|
|
50
|
+
desc: t.optional(t.string),
|
|
51
|
+
currentState: t.union(t.string, t.number),
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Only a single object is created - the object definition. Every other type union is a masking operation, and the result is an object of numbers.
|
|
56
|
+
|
|
57
|
+
In addition to these basic primitives, there are a number of additional special type validators:
|
|
58
|
+
|
|
59
|
+
| Definition | TypeScript type | Desc |
|
|
60
|
+
| ----------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| `t.const(value)` | Literal type of `value` | Constant value |
|
|
62
|
+
| `t.enum(...values)` | Union of literals | One of a set of constant values |
|
|
63
|
+
| `t.enum.caseInsensitive(...values)` | Union of literals | Case-insensitive set of values. All values get coerced to the casing in the _definition_. While not _recommended_, this is helpful for legacy APIs which may have inconsistent casing |
|
|
64
|
+
| `t.typename(value)` | Literal string | Type identifier for object and [Entity](/core/entities) types |
|
|
65
|
+
| `t.id` | `string \| number` | Identifier for [Entity](/core/entities) types |
|
|
66
|
+
| `t.result(type)` | `ParseResult<T>` | Parse result for explicit handling of parse errors |
|
|
67
|
+
| `t.format(name)` | Registered format type | Formatted string or number value, such as `date` or `date-time`. Formatted values are serialized and deserialized via a registered format function, and types are registered in a global registry |
|
|
68
|
+
| `t.entity(EntityClass)` | Entity class instance | An instance of the given [Entity](/core/entities) class |
|
|
69
|
+
|
|
70
|
+
## Designed for Evolving APIs
|
|
71
|
+
|
|
72
|
+
APIs change. New fields are added, new types appear in lists, response shapes grow. There are many small changes like these that generally don't warrant a "v2" of the API, and that may _appear_ additive and safely non-breaking at first, but that cause regressions when shipped in an app.
|
|
73
|
+
|
|
74
|
+
For instance, consider the following API Response:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
interface TextItem {
|
|
78
|
+
type: 'text';
|
|
79
|
+
content: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface ImageItem {
|
|
83
|
+
type: 'image';
|
|
84
|
+
url: string;
|
|
85
|
+
caption: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
type FeedItem = TextItem | ImageItem;
|
|
89
|
+
|
|
90
|
+
interface FeedResponse {
|
|
91
|
+
items: FeedItem[];
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Here we have a feed with two types of items, perhaps the initial MVP implementation of our feed page. We get it out the door and onto devices, and immediately product turns around and asks to add a Video feed item.
|
|
96
|
+
|
|
97
|
+
Now, if the developer of the client portion was thoughtful during the build out, they would have realized that even though there are only two items available currently, there _may_ be more in the future, and their implementation would have done something like the following:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { ImageFeedItem, TextFeedItem } from './feed-components';
|
|
101
|
+
|
|
102
|
+
export function Feed() {
|
|
103
|
+
const { items, isReady } = useFeedItems();
|
|
104
|
+
|
|
105
|
+
if (!isReady) return <div>Loading...</div>;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div>
|
|
109
|
+
{items.map((item) => {
|
|
110
|
+
switch (item.type) {
|
|
111
|
+
case 'text':
|
|
112
|
+
return <TextFeedItem item={item} />;
|
|
113
|
+
case 'image':
|
|
114
|
+
return <ImageFeedItem item={item} />;
|
|
115
|
+
default:
|
|
116
|
+
// Unknown feed item type, render nothing, log to telemetry
|
|
117
|
+
telemetry.log('unknown feed item type');
|
|
118
|
+
}
|
|
119
|
+
})}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
But maybe they didn't think about this, and instead threw an error if an unknown type was added. Or even before that, maybe their _type validators_ didn't account for the possibility of new types, and they throw an error on parse.
|
|
126
|
+
|
|
127
|
+
Fetchium's philosophy is based on _resilience_ to changes like these, and it achieves this through a core assumption:
|
|
128
|
+
|
|
129
|
+
> Showing _nothing_ is a better default than throwing an error.
|
|
130
|
+
|
|
131
|
+
This is not always true, there are times when you do want to show errors and tell users about explicit failures. But additive changes which have no impact on _existing clients_ out in the world should not cause failures by default. In the world of _native app_ development, where users may not upgrade for some time, this becomes exceedingly relevant.
|
|
132
|
+
|
|
133
|
+
Fetchium achieves this with two main default behaviors:
|
|
134
|
+
|
|
135
|
+
1. **Optional fields fall back gracefully.** If a field is wrapped in `t.optional(...)` and the incoming value doesn't match, it falls back to `undefined` instead of throwing a failure. This allows you to turn
|
|
136
|
+
- `t.optional(t.string)` into
|
|
137
|
+
- `t.optional(t.union(t.string, t.number))`
|
|
138
|
+
Without it being a breaking change to older clients.
|
|
139
|
+
2. **Arrays filter out unparseable items.** If an element in an array fails to parse, it's silently removed from the result rather than failing the entire response. This lets you add new types to a polymorphic array - clients that don't know about the new type simply don't see those items.
|
|
140
|
+
|
|
141
|
+
These two default behaviors are _safe_ because in both cases, we know that the code itself must be capable of handling it. In the case of optional fields, developers must already handle the `undefined` branch, and in the case of arrays, we simply skip over the unparsed items as if they didn't exist.
|
|
142
|
+
|
|
143
|
+
{% callout title="Logging parse failure events" type="note" %}
|
|
144
|
+
While these types of failures are typically expected, you may still want to know about them to understand if many clients are seeing a dramatic uptick in them. If every item in the array fails to parse, and users are seeing nothing in their feed, that is notable and still not ideal.
|
|
145
|
+
|
|
146
|
+
Failures can be captured by passing in a custom `log` object when creating the QueryClient. These failures are captured via `log.warn`, which has the same type signature as `console.warn`.
|
|
147
|
+
{% /callout %}
|
|
148
|
+
|
|
149
|
+
### Parse Results
|
|
150
|
+
|
|
151
|
+
You can also handle these failures more explicitly with `t.result`:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// Produces string | undefined
|
|
155
|
+
t.optional(t.string);
|
|
156
|
+
|
|
157
|
+
// Produces ParseResult<string>
|
|
158
|
+
t.result(t.string);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
When you wrap a type in `t.result`, it returns a parse result of the value:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
interface ParseSuccess<T> {
|
|
165
|
+
success: true;
|
|
166
|
+
value: T;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface ParseError {
|
|
170
|
+
success: false;
|
|
171
|
+
error: unknown;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
type ParseResult<T> = ParseSuccess<T> | ParseError;
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Parse results fail explicitly instead of silently and force the user to handle the error, ensure that it is safe to fail. They only apply to the _immediate_ child, so nested properties will still default to `undefined` if possible before attempting to throw.
|
|
178
|
+
|
|
179
|
+
## Unions and Performance
|
|
180
|
+
|
|
181
|
+
Unions are one of the trickiest parts of any parsing system. In particular, there are two main areas where things get difficult:
|
|
182
|
+
|
|
183
|
+
1. Unions of multiple different types of well-defined objects
|
|
184
|
+
2. Unions of unbounded _collections_ (e.g. arrays and records)
|
|
185
|
+
|
|
186
|
+
Unions of primitives types are fairly trivial (simply check `typeof`), and unions that include _one_ complex object type are also fairly straightforward, but when we need to distinguish between object shapes, it becomes much more difficult.
|
|
187
|
+
|
|
188
|
+
### Unions of Multiple Object Types
|
|
189
|
+
|
|
190
|
+
Imagine if we removed the `type` property from our earlier `FeedItem` example:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
interface TextItem {
|
|
194
|
+
content: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface ImageItem {
|
|
198
|
+
url: string;
|
|
199
|
+
caption: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type FeedItem = TextItem | ImageItem;
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The only way for us to tell if a `FeedItem` is a text or image item is through checking for the individual fields of one or the other. For validation libraries, this ends up meaning we parse _each type sequentially_ until we find one that matches the given object, which results in repeated traversals and errors being created and caught.
|
|
206
|
+
|
|
207
|
+
This is a massive performance penalty on one of the most common patterns in API unions, and libraries like Zod have added their own `discriminatedUnion` type functions to short circuit this by looking up a _discriminator field_, like our original `type` string in the first example. However, this strategy relies on developers _knowing and remembering_ to use a special type of union in these cases, which leaves you one small mistake away from a performance regression.
|
|
208
|
+
|
|
209
|
+
This brings us to Fetchium's _first_ major restriction on unions:
|
|
210
|
+
|
|
211
|
+
> **Object/entity unions must be discriminated.** When a union contains multiple object or entity types, each must have a _type_ field, denoted with `t.typename(...)`. This field can be _any_ field (you can call it `type` or `typename` or `__typename` or anything else that is a valid string), but ALL objects in a union must have the _same_ typename field, and each object must have a _unique_ typename _value_.
|
|
212
|
+
|
|
213
|
+
So for example, to define our `TextItem` and `ImageItem` types, we could do the following:
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
// ✅ Valid, same typename property with distinct values
|
|
217
|
+
const TextItem = t.object({
|
|
218
|
+
type: t.typename('text'),
|
|
219
|
+
content: t.string,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const ImageItem = t.object({
|
|
223
|
+
type: t.typename('image'),
|
|
224
|
+
url: t.string,
|
|
225
|
+
caption: t.string,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const FeedItem = t.union(TextItem, ImageItem);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
But these would be invalid:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
// 🛑 Invalid, different typenames
|
|
235
|
+
const TextItem = t.object({
|
|
236
|
+
type: t.typename('text'),
|
|
237
|
+
content: t.string,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const ImageItem = t.object({
|
|
241
|
+
typename: t.typename('image'),
|
|
242
|
+
url: t.string,
|
|
243
|
+
caption: t.string,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const FeedItem = t.union(TextItem, ImageItem);
|
|
247
|
+
|
|
248
|
+
// 🛑 Invalid, overlapping typenames
|
|
249
|
+
const TextItem = t.object({
|
|
250
|
+
type: t.typename('text'),
|
|
251
|
+
content: t.string,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const ExpandedTextItem = t.object({
|
|
255
|
+
typename: t.typename('text'),
|
|
256
|
+
content: t.string,
|
|
257
|
+
fullContent: t.string,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const FeedItem = t.union(TextItem, ExpandedTextItem);
|
|
261
|
+
|
|
262
|
+
// 🛑 Invalid, missing typenames on some objects
|
|
263
|
+
const TextItem = t.object({
|
|
264
|
+
content: t.string,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const ImageItem = t.object({
|
|
268
|
+
typename: t.typename('image'),
|
|
269
|
+
url: t.string,
|
|
270
|
+
caption: t.string,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const FeedItem = t.union(TextItem, ImageItem);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Unions of Collections
|
|
277
|
+
|
|
278
|
+
The other major pain point in parsing is _unions of collections_. To be clear, we are not talking about _collections of unions_. To illustrate:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
// Union of Collections
|
|
282
|
+
type UoC = string[] | number[] | TextItem[] | ImageItem[];
|
|
283
|
+
|
|
284
|
+
// Collection of Unions
|
|
285
|
+
type CoU = (string | number | TextItem | ImageItem)[];
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Collections of unions are actually completely fine, given they follow the previously established rules around `typename` for objects. But the reverse is difficult because of the potential for overlapping unions. Consider:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
type Overlapping = (string | number)[] | (string | boolean)[];
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
This type gives us a few problems:
|
|
295
|
+
|
|
296
|
+
- If we receive an array of _only_ strings, which type is it? We can't tell based on the value alone.
|
|
297
|
+
- If we receive an array with a number, many strings, and then a boolean, how do we maintain the context that we've already selected into one of the types?
|
|
298
|
+
- For even more complex type unions with more complex overlapping, how do we narrow progressively as we're parsing?
|
|
299
|
+
|
|
300
|
+
In other words, this is a can of worms for a behavior that is _fairly_ niche, and which can be solved (perhaps less ideally) by using a Collection of Unions instead. This leads to the _second_ major restriction Fetchium places on unions
|
|
301
|
+
|
|
302
|
+
> **Unions may only contain one type of each collection (records and arrays).** Unions may contain a record type and/or an array type, along with any number of primitive types and discriminated object types. But they cannot contain _more_ than one record or array type.
|
|
303
|
+
|
|
304
|
+
Some examples of valid and invalid collection unions:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
// ✅ Valid, array of unions
|
|
308
|
+
t.array(t.union(t.string, t.number));
|
|
309
|
+
|
|
310
|
+
// ✅ Valid, primitive + 1 array type
|
|
311
|
+
t.union(t.string, t.array(t.string));
|
|
312
|
+
|
|
313
|
+
// ✅ 1 array type + 1 record type
|
|
314
|
+
t.union(t.array(t.string), t.record(t.string));
|
|
315
|
+
|
|
316
|
+
// ✅ 1 array type + 1 record type
|
|
317
|
+
t.union(
|
|
318
|
+
t.array(t.union(t.string, t.number)),
|
|
319
|
+
t.record(t.union(t.string, t.number)),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// ✅ Valid, primitive + 1 array type + object type
|
|
323
|
+
t.union(t.string, t.array(t.string), t.object({ prop: t.string }));
|
|
324
|
+
|
|
325
|
+
// 🛑 Invalid, 2 array types
|
|
326
|
+
t.union(t.array(t.string), t.array(t.number));
|
|
327
|
+
|
|
328
|
+
// 🛑 Invalid, 2 record types
|
|
329
|
+
t.union(t.record(t.string), t.record(t.number));
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Formatted Values
|
|
333
|
+
|
|
334
|
+
Formats transform raw JSON values into richer types during parsing and serialize them back for caching. Two formats are included by default, matching the OpenAPI spec for formats:
|
|
335
|
+
|
|
336
|
+
| Format | Raw type | Parsed type | Description |
|
|
337
|
+
| ------------- | -------- | ----------- | -------------------------- |
|
|
338
|
+
| `'date'` | `string` | `Date` | `YYYY-MM-DD` parsed as UTC |
|
|
339
|
+
| `'date-time'` | `string` | `Date` | ISO 8601 string to Date |
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
startDate = t.format('date');
|
|
343
|
+
createdAt = t.format('date-time');
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Register custom formats with `registerFormat()`:
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
import { registerFormat, Mask } from 'fetchium';
|
|
350
|
+
|
|
351
|
+
registerFormat(
|
|
352
|
+
'currency',
|
|
353
|
+
Mask.STRING,
|
|
354
|
+
(raw) => parseFloat(raw.replace(/[$,]/g, '')),
|
|
355
|
+
(value) => `$${value.toFixed(2)}`,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// Then use it:
|
|
359
|
+
price = t.format('currency');
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
To add TypeScript types for custom formats, use module augmentation:
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
declare global {
|
|
366
|
+
namespace SignaliumQuery {
|
|
367
|
+
interface FormatRegistry {
|
|
368
|
+
'my-format': MyType;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
By default, formats are parsed eagerly. Pass `{ eager: false }` to defer parsing until the field is first read.
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Caching & Refetching
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Most data-fetching libraries treat caching as an afterthought --- a bag of imperative methods you call to set, invalidate, and evict cached data. You end up with `queryClient.invalidateQueries()` scattered across your codebase, mentally tracking which queries need refreshing after which mutations, and debugging stale UI when you forget one.
|
|
6
|
+
|
|
7
|
+
Fetchium takes a different approach. Caching in Fetchium is _declarative_. You configure three time-based knobs on your query classes --- `staleTime`, `gcTime`, and `cacheTime` --- and Fetchium handles the mechanics. The combination of entity normalization, automatic background refetching, and mutation effects keeps your data fresh without manual intervention. And when you do need to invalidate queries directly, you declare that on the mutation class too --- not in an imperative callback.
|
|
8
|
+
|
|
9
|
+
This philosophy is a direct extension of Fetchium's core design principle: _describe what you want, not how to get it_.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## The Three Time Knobs
|
|
14
|
+
|
|
15
|
+
Fetchium's cache behavior is controlled by three settings, each operating at a different layer of the caching system.
|
|
16
|
+
|
|
17
|
+
| Setting | Unit | Default | Scope | Description |
|
|
18
|
+
| ----------- | ------------ | ------------------ | ------------------------ | -------------------------------------------------------- |
|
|
19
|
+
| `staleTime` | milliseconds | `0` (always stale) | Per-query instance | How long data is considered fresh after fetching |
|
|
20
|
+
| `gcTime` | minutes | `5` | Per-query instance | How long an unwatched query stays in the in-memory cache |
|
|
21
|
+
| `cacheTime` | minutes | `1440` (24 hours) | Per-query class (static) | How long query results persist in the persistent store |
|
|
22
|
+
|
|
23
|
+
The lifecycle of a piece of data flows through these layers:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
fetch → in-memory cache (gcTime) → persistent store (cacheTime) → evicted
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
When a query is actively used by a component, it lives in memory. When all consumers unmount, the `gcTime` clock starts --- if no consumer re-subscribes before it expires, the data is evicted from memory. The persistent store (localStorage, IndexedDB) holds data independently and is governed by `cacheTime`. When a query is re-activated, Fetchium checks the store first --- if the cached data is newer than `cacheTime`, it is served immediately while a background refetch may occur (depending on `staleTime`).
|
|
30
|
+
|
|
31
|
+
### staleTime
|
|
32
|
+
|
|
33
|
+
`staleTime` controls how long data is considered _fresh_ after a successful fetch. While data is fresh, Fetchium serves it directly from the in-memory cache without hitting the network. Once the stale time has elapsed, the next read triggers a background refetch.
|
|
34
|
+
|
|
35
|
+
The default is `0`, meaning data is always considered stale. This is intentionally conservative --- every time a component mounts or re-activates, Fetchium will refetch in the background. For many applications this is exactly right: the network request happens transparently, the component shows cached data instantly, and updates arrive moments later.
|
|
36
|
+
|
|
37
|
+
Increase `staleTime` when:
|
|
38
|
+
|
|
39
|
+
- The data changes infrequently (user profiles, configuration, feature flags)
|
|
40
|
+
- The endpoint is expensive or rate-limited
|
|
41
|
+
- You want to reduce unnecessary network traffic on frequent navigation
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
class GetFeatureFlags extends RESTQuery {
|
|
45
|
+
path = '/feature-flags';
|
|
46
|
+
|
|
47
|
+
result = { flags: t.object({ darkMode: t.boolean, betaAccess: t.boolean }) };
|
|
48
|
+
|
|
49
|
+
config = {
|
|
50
|
+
staleTime: 5 * 60_000, // fresh for 5 minutes
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### gcTime
|
|
56
|
+
|
|
57
|
+
`gcTime` controls how long a query stays in the _in-memory_ cache after all of its consumers have unmounted. This is the window during which a user can navigate away and come back without triggering a new fetch --- the data is still in memory, ready to be served instantly.
|
|
58
|
+
|
|
59
|
+
The default is `5` minutes. Due to Fetchium's bucket-based garbage collection, the actual eviction time falls between `gcTime` and `2 × gcTime`. This is a deliberate trade-off: bucket-based GC is much cheaper than per-key timers, and the imprecision is irrelevant for most applications.
|
|
60
|
+
|
|
61
|
+
| `gcTime` value | Behavior |
|
|
62
|
+
| -------------- | -------------------------------------------------------------- |
|
|
63
|
+
| `0` | Evicted on the next tick after all consumers unmount |
|
|
64
|
+
| `5` (default) | Stays in memory for 5--10 minutes after last consumer unmounts |
|
|
65
|
+
| `Infinity` | Never evicted from memory (use with caution) |
|
|
66
|
+
|
|
67
|
+
### cacheTime
|
|
68
|
+
|
|
69
|
+
`cacheTime` controls how long query results persist in the _persistent store_ (localStorage, IndexedDB, or whatever `QueryStore` implementation you provide). When a query is activated and no in-memory data exists, Fetchium checks the store. If the cached entry is newer than `cacheTime`, it is loaded and served to the component immediately. If it is older, the stale entry is discarded and a fresh fetch happens.
|
|
70
|
+
|
|
71
|
+
The default is `1440` minutes (24 hours). Unlike `gcTime`, which is set per-instance via the `config` object, `cacheTime` is set per-query-class via the _static_ `cache` property. This is because persistent storage is shared across all instances of a query class.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Configuring Cache Behavior
|
|
76
|
+
|
|
77
|
+
### Per-instance config
|
|
78
|
+
|
|
79
|
+
`staleTime` and `gcTime` are set on the instance-level `config` field (or `getConfig()` method):
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
class GetUser extends RESTQuery {
|
|
83
|
+
params = { id: t.number };
|
|
84
|
+
|
|
85
|
+
path = `/users/${this.params.id}`;
|
|
86
|
+
|
|
87
|
+
result = { name: t.string, email: t.string };
|
|
88
|
+
|
|
89
|
+
config = {
|
|
90
|
+
staleTime: 30_000, // fresh for 30 seconds
|
|
91
|
+
gcTime: 10, // keep in memory 10 minutes after unmount
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For dynamic configuration based on runtime conditions, use the `getConfig()` method:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
class GetUser extends RESTQuery {
|
|
100
|
+
params = { id: t.number };
|
|
101
|
+
|
|
102
|
+
path = `/users/${this.params.id}`;
|
|
103
|
+
|
|
104
|
+
result = { name: t.string, email: t.string };
|
|
105
|
+
|
|
106
|
+
getConfig() {
|
|
107
|
+
const lastResponseOk = this.response?.ok ?? true;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
staleTime: lastResponseOk ? 30_000 : 0,
|
|
111
|
+
gcTime: 10,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Static cache for persistent store
|
|
118
|
+
|
|
119
|
+
`cacheTime` and `maxCount` are configured via the static `cache` property on the query class:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
class GetUser extends RESTQuery {
|
|
123
|
+
static cache = {
|
|
124
|
+
cacheTime: 120, // persist in store for 2 hours
|
|
125
|
+
maxCount: 100, // keep at most 100 cached instances
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
params = { id: t.number };
|
|
129
|
+
|
|
130
|
+
path = `/users/${this.params.id}`;
|
|
131
|
+
|
|
132
|
+
result = { name: t.string, email: t.string };
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`maxCount` limits how many distinct parameter combinations are stored for this query class. When the limit is exceeded, the oldest entries are evicted. The default is `50`.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Manual Refetching with `__refetch()`
|
|
141
|
+
|
|
142
|
+
The declarative cache covers the vast majority of use cases, but sometimes you need to explicitly trigger a fresh fetch. Every query result exposes a `__refetch()` method:
|
|
143
|
+
|
|
144
|
+
```tsx {% mode="react" %}
|
|
145
|
+
function UserProfile({ userId }: { userId: number }) {
|
|
146
|
+
const query = useQuery(GetUser, { id: userId });
|
|
147
|
+
|
|
148
|
+
if (!query.isReady) return <div>Loading...</div>;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div>
|
|
152
|
+
<h1>{query.value.name}</h1>
|
|
153
|
+
<button onClick={() => query.value.__refetch()}>Refresh</button>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
```tsx {% mode="signalium" %}
|
|
160
|
+
const UserProfile = component(({ userId }: { userId: number }) => {
|
|
161
|
+
const query = fetchQuery(GetUser, { id: userId });
|
|
162
|
+
|
|
163
|
+
if (!query.isReady) return <div>Loading...</div>;
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div>
|
|
167
|
+
<h1>{query.value.name}</h1>
|
|
168
|
+
<button onClick={() => query.value.__refetch()}>Refresh</button>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`__refetch()` returns the query's `ReactivePromise`, so you can `await` it if you need to know when the fresh data arrives:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const freshResult = await result.__refetch();
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
If a fetch is already in flight, `__refetch()` returns the existing promise without starting a duplicate request.
|
|
181
|
+
|
|
182
|
+
{% callout title="Why __refetch lives on the result" type="note" %}
|
|
183
|
+
You might wonder why `__refetch()` is on the `QueryResult` rather than on the `ReactivePromise`. The reason is composability: the result object can be passed to child components, stored in variables, or returned from helper functions. Any consumer holding a reference to the result can trigger a refetch without needing access to the original query handle.
|
|
184
|
+
{% /callout %}
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Cache Invalidation
|
|
189
|
+
|
|
190
|
+
If you are coming from TanStack Query, you may be used to `queryClient.invalidateQueries()` --- a single imperative call that marks queries as stale and triggers refetches. Fetchium handles this differently: invalidation is _declarative_ and happens through mutation effects, not imperative calls.
|
|
191
|
+
|
|
192
|
+
In a traditional query cache, each query is an island. When a mutation changes data, you have to manually figure out _which queries_ are affected and invalidate them in an `onSuccess` callback. Forget one, and you have stale UI. Add a new query that displays the same data, and you have to remember to add it to the invalidation list.
|
|
193
|
+
|
|
194
|
+
Fetchium's entity normalization eliminates this problem for most cases. When User #42's name changes via a mutation, _every query displaying User #42 updates automatically_, because they all share the same normalized entity proxy. You do not need to know which queries to invalidate --- the entity system handles it.
|
|
195
|
+
|
|
196
|
+
The mechanisms for keeping data fresh in Fetchium are, in order of preference:
|
|
197
|
+
|
|
198
|
+
1. **Entity effects from mutations** (`creates`, `updates`, `deletes`). When a mutation succeeds, its effects update entities in the normalized store. Every query referencing those entities sees the change immediately. This is the most common and most powerful freshness mechanism.
|
|
199
|
+
|
|
200
|
+
2. **Query invalidation via `invalidates`**. For mutations whose effects are too complex or broad to express as entity-level changes, you can declare `invalidates` in the mutation's effects to mark specific query classes as stale. You can target all instances of a query class, or only those matching a param subset. See the [Mutations guide](/data/mutations) for details.
|
|
201
|
+
|
|
202
|
+
3. **`staleTime` expiration.** Background refetches happen automatically when data becomes stale and a consumer re-reads it. For data that changes unpredictably on the server, this provides eventual consistency without any manual intervention.
|
|
203
|
+
|
|
204
|
+
4. **`__refetch()` for explicit reloads.** When you need a one-off full query reload from a specific place in your code (not tied to a mutation), `__refetch()` on the query result gives you a direct escape hatch.
|
|
205
|
+
|
|
206
|
+
5. **`refreshStaleOnReconnect` for network recovery.** When the user's device comes back online, stale queries are automatically refetched.
|
|
207
|
+
|
|
208
|
+
{% callout type="note" %}
|
|
209
|
+
Entity effects should be your first choice. They are precise, efficient, and work with optimistic updates and live data. Use `invalidates` when the mutation's impact is too broad to express as entity changes. Use `__refetch()` only for one-off imperative needs outside of the mutation system.
|
|
210
|
+
{% /callout %}
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Background Refetching
|
|
215
|
+
|
|
216
|
+
Fetchium automatically refetches data in several scenarios, all governed by the `staleTime` setting.
|
|
217
|
+
|
|
218
|
+
### On remount
|
|
219
|
+
|
|
220
|
+
When a component mounts and subscribes to a query that already has cached data, Fetchium serves the cached data immediately. If the data is stale (older than `staleTime`), a background refetch is triggered. The component sees the cached data first, then re-renders with fresh data when the fetch completes.
|
|
221
|
+
|
|
222
|
+
This is why the default `staleTime` of `0` works well in practice: users see data instantly, and it silently refreshes in the background. The UI never shows a loading spinner for data that has been fetched before.
|
|
223
|
+
|
|
224
|
+
### On reconnect
|
|
225
|
+
|
|
226
|
+
The `refreshStaleOnReconnect` option (default: `true`) controls whether stale queries are automatically refetched when the device comes back online. When a user loses connectivity and then reconnects, all active queries whose data has become stale are refetched automatically.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
class GetUser extends RESTQuery {
|
|
230
|
+
params = { id: t.number };
|
|
231
|
+
|
|
232
|
+
path = `/users/${this.params.id}`;
|
|
233
|
+
|
|
234
|
+
result = { name: t.string, email: t.string };
|
|
235
|
+
|
|
236
|
+
config = {
|
|
237
|
+
refreshStaleOnReconnect: false, // opt out of automatic reconnect refetch
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Stale vs. pending
|
|
243
|
+
|
|
244
|
+
It is worth understanding the distinction between _stale_ and _pending_:
|
|
245
|
+
|
|
246
|
+
- **Stale** means the data's age has exceeded `staleTime`. Stale data is still perfectly usable --- it is served to the component and displayed to the user. It simply means a background refetch will be triggered on the next activation.
|
|
247
|
+
- **Pending** means a fetch is currently in flight. A query can be both stale and pending simultaneously (it has old data and is fetching new data).
|
|
248
|
+
|
|
249
|
+
The `ReactivePromise` exposes both states: `isPending` tells you if a fetch is in progress, while `isReady` tells you if _any_ value (fresh or stale) is available. For most UIs, you should use `isReady` to decide whether to render content, not `isPending`.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Entity-Level GC
|
|
254
|
+
|
|
255
|
+
Entities have their own garbage collection, configured via the static `cache` property on the `Entity` class:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
class User extends Entity {
|
|
259
|
+
static cache = { gcTime: 10 }; // keep in memory 10 minutes after last reference
|
|
260
|
+
|
|
261
|
+
__typename = t.typename('User');
|
|
262
|
+
id = t.id;
|
|
263
|
+
|
|
264
|
+
name = t.string;
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
| `gcTime` value | Behavior |
|
|
269
|
+
| --------------------- | ------------------------------------------------------------- |
|
|
270
|
+
| `undefined` (default) | Entity is evicted immediately when no queries reference it |
|
|
271
|
+
| `0` | Entity is evicted on the next tick |
|
|
272
|
+
| `10` | Entity stays in cache for 10--20 minutes after last reference |
|
|
273
|
+
| `Infinity` | Entity is never garbage collected |
|
|
274
|
+
|
|
275
|
+
Entity GC and query GC are independent but related. When a query is evicted from memory (its `gcTime` expires), the entities it referenced lose one consumer. If no other active query references a given entity, that entity's own `gcTime` clock starts. This means:
|
|
276
|
+
|
|
277
|
+
- If both the query and entity have a `gcTime` of 5, an entity could stay in memory for up to 5 + 10 = 15 minutes after the last component unmounts (query GC window + entity GC window, accounting for bucket imprecision).
|
|
278
|
+
- Setting entity `gcTime` to `Infinity` keeps entities in memory permanently --- useful for user profiles or other data that is referenced from many queries and expensive to re-parse.
|
|
279
|
+
|
|
280
|
+
{% callout %}
|
|
281
|
+
Entity cache configuration is set at the class level, not per-instance. All instances of `User` share the same GC policy regardless of which query loaded them.
|
|
282
|
+
{% /callout %}
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Next Steps
|
|
287
|
+
|
|
288
|
+
{% quick-links %}
|
|
289
|
+
|
|
290
|
+
{% quick-link title="Mutations" icon="theming" href="/data/mutations" description="Update data with optimistic updates and entity effects" /%}
|
|
291
|
+
|
|
292
|
+
{% quick-link title="Offline & Persistence" icon="installation" href="/guides/offline" description="Configure persistent stores and offline-first network modes" /%}
|
|
293
|
+
|
|
294
|
+
{% quick-link title="REST Queries Reference" icon="presets" href="/reference/rest-queries" description="Full reference for query fields, methods, and configuration" /%}
|
|
295
|
+
|
|
296
|
+
{% quick-link title="Entities" icon="plugins" href="/core/entities" description="Normalized entity caching and identity-stable proxies" /%}
|
|
297
|
+
|
|
298
|
+
{% /quick-links %}
|