fetchium 0.1.0 → 0.1.1
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 +6 -5
- package/dist/cjs/development/QueryClient-CLi3ONNM.js +2 -0
- package/dist/cjs/development/QueryClient-CLi3ONNM.js.map +1 -0
- package/dist/cjs/development/QueryController-BQA49OYU.js +2 -0
- package/dist/cjs/development/QueryController-BQA49OYU.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-CikIl_6k.js +2 -0
- package/dist/cjs/development/mutation-CikIl_6k.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/QueryClient-N0MJmuHW.js +2 -0
- package/dist/cjs/production/QueryClient-N0MJmuHW.js.map +1 -0
- package/dist/cjs/production/QueryController-BQA49OYU.js +2 -0
- package/dist/cjs/production/QueryController-BQA49OYU.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-P_Yb4LI9.js +2 -0
- package/dist/cjs/production/mutation-P_Yb4LI9.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/QueryClient.d.ts +26 -4
- package/dist/esm/QueryClient.d.ts.map +1 -1
- package/dist/esm/QueryController.d.ts +49 -0
- package/dist/esm/QueryController.d.ts.map +1 -0
- package/dist/esm/QueryResult.d.ts +10 -10
- package/dist/esm/QueryResult.d.ts.map +1 -1
- package/dist/esm/development/QueryClient-Dtde3pss.js +2572 -0
- package/dist/esm/development/QueryClient-Dtde3pss.js.map +1 -0
- package/dist/esm/development/QueryController-Ch_ncxiI.js +14 -0
- package/dist/esm/development/QueryController-Ch_ncxiI.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-UZshUQAf.js +58 -0
- package/dist/esm/development/mutation-UZshUQAf.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/{QueryClient-BP0Z1rQV.js → QueryClient-YqnBxFy1.js} +972 -968
- package/dist/esm/production/QueryClient-YqnBxFy1.js.map +1 -0
- package/dist/esm/production/QueryController-Ch_ncxiI.js +14 -0
- package/dist/esm/production/QueryController-Ch_ncxiI.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-pgFl1uIY.js +58 -0
- package/dist/esm/production/mutation-pgFl1uIY.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/RESTQueryController.d.ts +34 -0
- package/dist/esm/rest/RESTQueryController.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/TopicQueryController.d.ts +43 -0
- package/dist/esm/topic/TopicQueryController.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,435 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Live Data
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Fetchium can keep collections and values **automatically up-to-date** as entities are created, updated, or deleted -- whether through mutations, streaming, or any other source of entity events. This is built on two primitives: **LiveArray** and **LiveValue**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Live Arrays
|
|
10
|
+
|
|
11
|
+
A LiveArray is a reactive list of entities that automatically updates when matching entities are created, updated, or deleted --- via mutations, streaming, or any other source of entity events. Instead of returning a static snapshot, the array stays in sync with the entity store.
|
|
12
|
+
|
|
13
|
+
The key to making a live array _reactive to external events_ is **constraints**. Constraints define _which_ entities belong in the array, and they are what enable Fetchium to route mutation and streaming events to the correct arrays.
|
|
14
|
+
|
|
15
|
+
Here is a typical example --- a `List` entity whose `items` field is a live array of `Item` entities scoped by `listId`:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { Entity, t } from 'fetchium';
|
|
19
|
+
import { RESTQuery } from 'fetchium/rest';
|
|
20
|
+
|
|
21
|
+
class Item extends Entity {
|
|
22
|
+
__typename = t.typename('Item');
|
|
23
|
+
id = t.id;
|
|
24
|
+
|
|
25
|
+
listId = t.string;
|
|
26
|
+
name = t.string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class List extends Entity {
|
|
30
|
+
__typename = t.typename('List');
|
|
31
|
+
id = t.id;
|
|
32
|
+
|
|
33
|
+
items = t.liveArray(Item, {
|
|
34
|
+
constraints: { listId: this.id },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class GetList extends RESTQuery {
|
|
39
|
+
params = { id: t.id };
|
|
40
|
+
|
|
41
|
+
path = `/lists/${this.params.id}`;
|
|
42
|
+
|
|
43
|
+
result = { list: t.entity(List) };
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
When the query resolves, `list.items` is a reactive array. It starts with whatever the server returned, but when a mutation or stream creates a new `Item` whose `listId` matches this list's `id`, the item is **automatically added**. When an `Item` is deleted, it is **automatically removed**.
|
|
48
|
+
|
|
49
|
+
```tsx {% mode="react" %}
|
|
50
|
+
import { useQuery } from 'fetchium/react';
|
|
51
|
+
|
|
52
|
+
function ItemList({ listId }: { listId: string }) {
|
|
53
|
+
const result = useQuery(GetList, { id: listId });
|
|
54
|
+
|
|
55
|
+
if (!result.isReady) return <div>Loading...</div>;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<ul>
|
|
59
|
+
{result.value.list.items.map((item) => (
|
|
60
|
+
<li key={item.id}>{item.name}</li>
|
|
61
|
+
))}
|
|
62
|
+
</ul>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```tsx {% mode="signalium" %}
|
|
68
|
+
import { fetchQuery } from 'fetchium';
|
|
69
|
+
import { component } from 'signalium/react';
|
|
70
|
+
|
|
71
|
+
const ItemList = component(({ listId }: { listId: string }) => {
|
|
72
|
+
const result = fetchQuery(GetList, { id: listId });
|
|
73
|
+
|
|
74
|
+
if (!result.isReady) return <div>Loading...</div>;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<ul>
|
|
78
|
+
{result.value.list.items.map((item) => (
|
|
79
|
+
<li key={item.id}>{item.name}</li>
|
|
80
|
+
))}
|
|
81
|
+
</ul>
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Constraints
|
|
89
|
+
|
|
90
|
+
Constraints are what make live arrays _reactive to external events_. They filter which entities belong in the array, and they are what enable Fetchium to route mutation and streaming events to the correct live arrays.
|
|
91
|
+
|
|
92
|
+
A common pattern is to scope a live array to a parent entity. For example, a `List` entity whose items are scoped by `listId`:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
class Item extends Entity {
|
|
96
|
+
__typename = t.typename('Item');
|
|
97
|
+
id = t.id;
|
|
98
|
+
listId = t.string;
|
|
99
|
+
name = t.string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class List extends Entity {
|
|
103
|
+
__typename = t.typename('List');
|
|
104
|
+
id = t.id;
|
|
105
|
+
items = t.liveArray(Item, {
|
|
106
|
+
constraints: { listId: this.id },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
In this example, `this.id` is a **field reference** --- it resolves to the current `List` entity's `id` value at runtime. When a new `Item` is created (via a mutation effect, streaming event, or `applyMutationEvent`), Fetchium checks whether its `listId` matches this list's `id`. If it does, the item is added to the array. If not, it is ignored.
|
|
112
|
+
|
|
113
|
+
This means if you have two lists (List #1 and List #2), creating an Item with `listId: '1'` will only add it to List #1's `items` array.
|
|
114
|
+
|
|
115
|
+
### Static constraint values
|
|
116
|
+
|
|
117
|
+
You can also use literal values as constraints:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
class GetActiveUsers extends RESTQuery {
|
|
121
|
+
path = '/users';
|
|
122
|
+
|
|
123
|
+
searchParams = { status: 'active' };
|
|
124
|
+
|
|
125
|
+
result = {
|
|
126
|
+
users: t.liveArray(User, {
|
|
127
|
+
constraints: { status: 'active' },
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Only `User` entities whose `status` field is `'active'` will be added to this live array. If a mutation creates a user with `status: 'inactive'`, it will not appear here.
|
|
134
|
+
|
|
135
|
+
### Multiple entity types
|
|
136
|
+
|
|
137
|
+
You can pass an array of entity classes to `t.liveArray` to watch for multiple entity types. Each entity type still needs constraints to react to external events:
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
class Notification extends Entity {
|
|
141
|
+
__typename = t.typename('Notification');
|
|
142
|
+
id = t.id;
|
|
143
|
+
|
|
144
|
+
userId = t.string;
|
|
145
|
+
message = t.string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class Alert extends Entity {
|
|
149
|
+
__typename = t.typename('Alert');
|
|
150
|
+
id = t.id;
|
|
151
|
+
|
|
152
|
+
userId = t.string;
|
|
153
|
+
message = t.string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
class UserInbox extends Entity {
|
|
157
|
+
__typename = t.typename('UserInbox');
|
|
158
|
+
id = t.id;
|
|
159
|
+
|
|
160
|
+
items = t.liveArray([Notification, Alert], {
|
|
161
|
+
constraints: { userId: this.id },
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
This live array reacts to both `Notification` and `Alert` entity events, but only when the entity's `userId` matches the inbox's `id`.
|
|
167
|
+
|
|
168
|
+
{% callout type="warning" %}
|
|
169
|
+
When using constraints with field references like `this.id`, the field reference captures the **parent entity's** value at the time the live array is initialized. Make sure the referenced field is part of the same entity definition.
|
|
170
|
+
{% /callout %}
|
|
171
|
+
|
|
172
|
+
### Unconstrained live arrays
|
|
173
|
+
|
|
174
|
+
A LiveArray _without_ constraints is **local-only**. It accumulates items from the query's own fetches (including `__fetchNext` / pagination and subscription data), but does _not_ react to external mutation events or other queries' data.
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
class GetItems extends RESTQuery {
|
|
178
|
+
path = '/items';
|
|
179
|
+
|
|
180
|
+
result = { items: t.liveArray(Item) };
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
This is useful for pagination --- when you call `__fetchNext()`, new entities are appended to the live array. But a mutation with a `creates` effect for `Item` will _not_ add items to this array, because there are no constraints to match against.
|
|
185
|
+
|
|
186
|
+
To make a live array react to creates and deletes from mutations, streaming, or `applyMutationEvent`, you must provide constraints.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Sorting
|
|
191
|
+
|
|
192
|
+
Keep live arrays sorted by providing a `sort` function. The sort function follows the same contract as `Array.prototype.sort` -- it receives two items and returns a negative number, zero, or positive number.
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
class GetActiveUsers extends RESTQuery {
|
|
196
|
+
path = '/users';
|
|
197
|
+
|
|
198
|
+
result = {
|
|
199
|
+
users: t.liveArray(User, {
|
|
200
|
+
constraints: { status: 'active' },
|
|
201
|
+
sort(a, b) {
|
|
202
|
+
return a.name.localeCompare(b.name);
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
When entities are added to the array (via creation events), the sort order is maintained. When entity data changes (e.g. a user's name is updated), the array is re-sorted.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Live Values
|
|
214
|
+
|
|
215
|
+
A LiveValue is a single reactive value that updates in response to entity events. While LiveArrays track lists, LiveValues are useful for **computed aggregates** like counts, totals, sums, or any derived scalar.
|
|
216
|
+
|
|
217
|
+
Define a live value using `t.liveValue(valueType, EntityClass, options)`:
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
class List extends Entity {
|
|
221
|
+
__typename = t.typename('List');
|
|
222
|
+
id = t.id;
|
|
223
|
+
|
|
224
|
+
items = t.liveArray(Item, {
|
|
225
|
+
constraints: { listId: this.id },
|
|
226
|
+
});
|
|
227
|
+
itemCount = t.liveValue(t.number, Item, {
|
|
228
|
+
constraints: { listId: this.id },
|
|
229
|
+
onCreate: (count, _item) => count + 1,
|
|
230
|
+
onUpdate: (count, _item) => count,
|
|
231
|
+
onDelete: (count, _item) => count - 1,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
The three reducer callbacks control how the value changes in response to entity events:
|
|
237
|
+
|
|
238
|
+
| Callback | When it fires | Arguments |
|
|
239
|
+
| ---------- | -------------------------------- | ------------------------------- |
|
|
240
|
+
| `onCreate` | A new matching entity is created | `(currentValue, newEntity)` |
|
|
241
|
+
| `onUpdate` | A matching entity is updated | `(currentValue, updatedEntity)` |
|
|
242
|
+
| `onDelete` | A matching entity is deleted | `(currentValue, deletedEntity)` |
|
|
243
|
+
|
|
244
|
+
Each callback receives the current accumulated value and the entity involved, and returns the new value.
|
|
245
|
+
|
|
246
|
+
### Initial value
|
|
247
|
+
|
|
248
|
+
The initial value of a live value comes from the server response. In the example above, if the server returns `{ itemCount: 3, items: [...] }`, the `itemCount` starts at `3`. Subsequent create/delete events increment or decrement from there.
|
|
249
|
+
|
|
250
|
+
### Example: tracking a total
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
class Order extends Entity {
|
|
254
|
+
__typename = t.typename('Order');
|
|
255
|
+
id = t.id;
|
|
256
|
+
|
|
257
|
+
customerId = t.string;
|
|
258
|
+
total = t.number;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
class Customer extends Entity {
|
|
262
|
+
__typename = t.typename('Customer');
|
|
263
|
+
id = t.id;
|
|
264
|
+
|
|
265
|
+
name = t.string;
|
|
266
|
+
orderTotal = t.liveValue(t.number, Order, {
|
|
267
|
+
constraints: { customerId: this.id },
|
|
268
|
+
onCreate: (sum, order) => sum + order.total,
|
|
269
|
+
onUpdate: (sum, _order) => sum,
|
|
270
|
+
onDelete: (sum, order) => sum - order.total,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```tsx {% mode="react" %}
|
|
276
|
+
import { useQuery } from 'fetchium/react';
|
|
277
|
+
|
|
278
|
+
function CustomerSummary() {
|
|
279
|
+
const { customer } = useQuery(GetCustomer, { id: '1' });
|
|
280
|
+
|
|
281
|
+
// `orderTotal` updates automatically as orders are created/deleted
|
|
282
|
+
return (
|
|
283
|
+
<div>
|
|
284
|
+
<h1>{customer.name}</h1>
|
|
285
|
+
<p>Total spent: ${customer.orderTotal}</p>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```tsx {% mode="signalium" %}
|
|
292
|
+
import { fetchQuery } from 'fetchium';
|
|
293
|
+
import { component } from 'signalium/react';
|
|
294
|
+
|
|
295
|
+
const CustomerSummary = component(() => {
|
|
296
|
+
const { customer } = fetchQuery(GetCustomer, { id: '1' });
|
|
297
|
+
|
|
298
|
+
// `orderTotal` updates automatically as orders are created/deleted
|
|
299
|
+
return (
|
|
300
|
+
<div>
|
|
301
|
+
<h1>{customer.name}</h1>
|
|
302
|
+
<p>Total spent: ${customer.orderTotal}</p>
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
{% callout %}
|
|
309
|
+
LiveValue reducers are only triggered by **mutation events and streaming updates**, not by initial server fetches. When the server returns data, the initial value from the response is used as-is. This prevents double-counting entities that were already included in the server response.
|
|
310
|
+
{% /callout %}
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## How Live Data Works
|
|
315
|
+
|
|
316
|
+
Under the hood, live data is powered by Fetchium's entity event system:
|
|
317
|
+
|
|
318
|
+
1. **An event fires.** When a mutation completes, a stream delivers an update, or you call `applyMutationEvent` manually, Fetchium fires an entity event (`create`, `update`, or `delete`) with the typename and entity data.
|
|
319
|
+
|
|
320
|
+
2. **Constraints are checked.** Fetchium finds all live arrays and live values watching that typename, and checks whether the entity's data satisfies their constraints. Only matching collections receive the event.
|
|
321
|
+
|
|
322
|
+
3. **The UI updates.** Matching live arrays add or remove the entity. Matching live values run their reducer. Any component reading from those fields re-renders automatically.
|
|
323
|
+
|
|
324
|
+
This design means live data is **fully local** --- it does not require any special server protocol. Any source of entity events (mutations, streaming, polling, or manual `applyMutationEvent` calls) flows through the same pipeline.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Pagination & Infinite Queries
|
|
329
|
+
|
|
330
|
+
Fetchium supports cursor-based and offset-based pagination via the `fetchNext` configuration on queries. Live arrays work seamlessly with pagination -- when you load additional pages, new entities are **appended** to the existing live array rather than replacing it.
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
class GetItems extends RESTQuery {
|
|
334
|
+
path = '/items';
|
|
335
|
+
|
|
336
|
+
result = {
|
|
337
|
+
items: t.liveArray(Item),
|
|
338
|
+
nextCursor: t.optional(t.string),
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
fetchNext = {
|
|
342
|
+
searchParams: {
|
|
343
|
+
cursor: this.result.nextCursor,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
After fetching, call `__fetchNext()` on the query result to fetch the next page:
|
|
350
|
+
|
|
351
|
+
```tsx {% mode="react" %}
|
|
352
|
+
import { useQuery } from 'fetchium/react';
|
|
353
|
+
|
|
354
|
+
function ItemList() {
|
|
355
|
+
const result = useQuery(GetItems);
|
|
356
|
+
|
|
357
|
+
if (!result.isReady) return <div>Loading...</div>;
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<div>
|
|
361
|
+
<ul>
|
|
362
|
+
{result.value.items.map((item) => (
|
|
363
|
+
<li key={item.id}>{item.name}</li>
|
|
364
|
+
))}
|
|
365
|
+
</ul>
|
|
366
|
+
{result.__hasNext && (
|
|
367
|
+
<button onClick={() => result.__fetchNext()}>Load more</button>
|
|
368
|
+
)}
|
|
369
|
+
</div>
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
```tsx {% mode="signalium" %}
|
|
375
|
+
import { fetchQuery } from 'fetchium';
|
|
376
|
+
import { component } from 'signalium/react';
|
|
377
|
+
|
|
378
|
+
const ItemList = component(() => {
|
|
379
|
+
const result = fetchQuery(GetItems);
|
|
380
|
+
|
|
381
|
+
if (!result.isReady) return <div>Loading...</div>;
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<div>
|
|
385
|
+
<ul>
|
|
386
|
+
{result.value.items.map((item) => (
|
|
387
|
+
<li key={item.id}>{item.name}</li>
|
|
388
|
+
))}
|
|
389
|
+
</ul>
|
|
390
|
+
{result.__hasNext && (
|
|
391
|
+
<button onClick={() => result.__fetchNext()}>Load more</button>
|
|
392
|
+
)}
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
The `fetchNext.searchParams` object uses **field references** (`this.result.nextCursor`) to automatically pull pagination cursors from the previous response. Each call to `__fetchNext()` fetches the next page and appends entities to the live array.
|
|
399
|
+
|
|
400
|
+
{% callout %}
|
|
401
|
+
For non-live arrays (`t.array` instead of `t.liveArray`), `__fetchNext()` **replaces** the array contents with the new page. Only `t.liveArray` accumulates across pages.
|
|
402
|
+
{% /callout %}
|
|
403
|
+
|
|
404
|
+
For full details on pagination patterns, including offset-based pagination and conditional loading, see the [Pagination reference](/reference/pagination).
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Streaming Updates
|
|
409
|
+
|
|
410
|
+
Entities can subscribe to real-time updates by defining a `__subscribe` method on the entity class. When an entity with `__subscribe` is actively observed (read by a component or reactive function), Fetchium establishes the subscription and routes incoming events through the live data system.
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
class ChatMessage extends Entity {
|
|
414
|
+
__typename = t.typename('ChatMessage');
|
|
415
|
+
id = t.id;
|
|
416
|
+
|
|
417
|
+
channelId = t.string;
|
|
418
|
+
text = t.string;
|
|
419
|
+
author = t.entity(User);
|
|
420
|
+
|
|
421
|
+
__subscribe(onEvent) {
|
|
422
|
+
const es = new EventSource(`/api/messages/${this.id}/stream`);
|
|
423
|
+
es.onmessage = (e) => {
|
|
424
|
+
onEvent(JSON.parse(e.data));
|
|
425
|
+
};
|
|
426
|
+
return () => es.close();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
When the stream delivers a `create` event for a `ChatMessage`, any constrained live array watching `ChatMessage` entities (whose constraints match the new message's data) will automatically include it. When it delivers a `delete` event, the message is removed from matching arrays.
|
|
432
|
+
|
|
433
|
+
This makes it straightforward to build real-time features: define your entities with `__subscribe`, use `t.liveArray` or `t.liveValue` in your result shapes, and the UI updates automatically.
|
|
434
|
+
|
|
435
|
+
For full details on streaming patterns and transport options, see the [Streaming guide](/core/streaming).
|