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,351 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Entities
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Queries may be the foundation of Fetchium's data model, but _entities_ are its connective tissue. They provide **normalized, deduplicated data objects** that are shared across _all_ queries. When the same entity (identified via typename + id) is returned by multiple queries, they all share the same object reference -- meaning updates to an entity from any source are immediately visible everywhere.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Defining an Entity
|
|
10
|
+
|
|
11
|
+
To define an entity, extend the `Entity` class and declare fields using the type DSL. Every entity must have a typename and an ID, defined with `t.typename` and `t.id` respectively.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Entity, t } from 'fetchium';
|
|
15
|
+
|
|
16
|
+
class User extends Entity {
|
|
17
|
+
__typename = t.typename('User');
|
|
18
|
+
id = t.id;
|
|
19
|
+
|
|
20
|
+
name = t.string;
|
|
21
|
+
email = t.string;
|
|
22
|
+
avatar = t.optional(t.string);
|
|
23
|
+
createdAt = t.format('date-time');
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Every entity is uniquely identified by the combination of its **typename** and **id**. The typename is used in a variety of cases, including _type discrimination_ for unions, streaming updates, and normalization and deduplication.
|
|
28
|
+
|
|
29
|
+
Entities can be referenced using `t.entity` in queries or in other entities:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
class User extends Entity {
|
|
33
|
+
__typename = t.typename('User');
|
|
34
|
+
id = t.id;
|
|
35
|
+
|
|
36
|
+
name = t.string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class Post extends Entity {
|
|
40
|
+
__typename = t.typename('Post');
|
|
41
|
+
id = t.id;
|
|
42
|
+
|
|
43
|
+
title = t.string;
|
|
44
|
+
author = t.entity(User);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class GetPost extends RESTQuery {
|
|
48
|
+
params = {
|
|
49
|
+
id: t.string,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
path = `/posts/${this.params.id}`;
|
|
53
|
+
|
|
54
|
+
result = {
|
|
55
|
+
post: t.entity(Post),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
{% callout type="warning" %}
|
|
61
|
+
Entities are **read-only**. Attempting to set a property on an entity will throw an error in development mode. To update entity data, use mutations or streaming updates.
|
|
62
|
+
{% /callout %}
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Signalium Feature: Identity-Stable Proxies
|
|
67
|
+
|
|
68
|
+
When Fetchium parses a query response, it does not return plain JavaScript objects for entities. Instead, it returns **Proxy objects** that are tied to the normalized entity store.
|
|
69
|
+
|
|
70
|
+
The key property of these proxies is **identity stability**: for any given `(typename, id)` pair, Fetchium always returns the **same proxy object**. This has several important consequences:
|
|
71
|
+
|
|
72
|
+
- **Reference equality across queries.** If `GetUser` and `GetPostWithAuthor` both return User #42, the `user` object in both results is the exact same proxy (`===`).
|
|
73
|
+
- **Automatic updates.** When an entity's data changes (from a refetch, mutation, or stream), the proxy reflects the new data immediately. Any component or reactive function reading from that proxy sees the update.
|
|
74
|
+
- **Safe to store in state.** You can save an entity proxy in local state or pass it as a prop. It will never go stale -- it always points to the latest data in the cache.
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
// Two different queries returning the same user
|
|
78
|
+
const { user } = await fetchQuery(GetUser, { id: '1' });
|
|
79
|
+
const { post } = await fetchQuery(GetPostWithAuthor, { postId: '5' });
|
|
80
|
+
|
|
81
|
+
// If post #5's author is user #1, these are the exact same object
|
|
82
|
+
user === post.author; // true
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Each proxy object is backed by a single signal, which is notified whenever the entity is updated. Entanglement of these signals is _lazy_, meaning that if you do not use any properties, you do not pay the cost:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
const getPostTitle = reactive(async () => {
|
|
89
|
+
const { post } = await fetchQuery(GetPostWithAuthor, { postId: '5' });
|
|
90
|
+
|
|
91
|
+
if (post.showAuthor) {
|
|
92
|
+
// Author signal is entangled if this branch is taken
|
|
93
|
+
return `${post.title} - by ${post.author.name}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Author signal is ignored if this one is taken
|
|
97
|
+
return post.title;
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### React Behavior
|
|
102
|
+
|
|
103
|
+
When crossing the boundary between Fetchium/Signalium into React via `useQuery` or `useReactive`, the value is _deeply cloned_ with _structural sharing_. This means that duplicate objects will be created for each reactive barrier, and values will be _eagerly_ entangled.
|
|
104
|
+
|
|
105
|
+
While this is somewhat less performant, it is also inline with React's expectations for state management, and structural sharing prevents excessive invalidation from optimizations by `useMemo`, `React.memo`, and the React compiler.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Nested Entities
|
|
110
|
+
|
|
111
|
+
Entities can reference other entities using `t.entity(EntityClass)`. Nested entities are also normalized and deduplicated -- they follow all the same rules as top-level entities.
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
class Comment extends Entity {
|
|
115
|
+
__typename = t.typename('Comment');
|
|
116
|
+
id = t.id;
|
|
117
|
+
|
|
118
|
+
body = t.string;
|
|
119
|
+
author = t.entity(User);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class Post extends Entity {
|
|
123
|
+
__typename = t.typename('Post');
|
|
124
|
+
id = t.id;
|
|
125
|
+
|
|
126
|
+
title = t.string;
|
|
127
|
+
body = t.string;
|
|
128
|
+
author = t.entity(User);
|
|
129
|
+
comments = t.array(t.entity(Comment));
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
In this example, if a `Post` and one of its `Comment`s reference the same `User`, both `post.author` and `comment.author` will be the same proxy object. Updating that user's name via any query will update it in both places.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
class GetPost extends RESTQuery {
|
|
137
|
+
params = { id: t.id };
|
|
138
|
+
|
|
139
|
+
path = `/posts/${this.params.id}`;
|
|
140
|
+
|
|
141
|
+
result = { post: t.entity(Post) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// After fetching:
|
|
145
|
+
const post = result.post;
|
|
146
|
+
const firstComment = post.comments[0];
|
|
147
|
+
|
|
148
|
+
// If the post author and first comment author are the same user:
|
|
149
|
+
post.author === firstComment.author; // true
|
|
150
|
+
post.author.name; // "Alice"
|
|
151
|
+
firstComment.author.name; // "Alice" (same object)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Entity Methods
|
|
157
|
+
|
|
158
|
+
You can define methods directly on entity classes. Methods have access to the entity's fields via `this` and are automatically wrapped with `reactiveMethod` for memoization -- meaning the same arguments produce the same result without recomputation.
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
class User extends Entity {
|
|
162
|
+
__typename = t.typename('User');
|
|
163
|
+
id = t.id;
|
|
164
|
+
|
|
165
|
+
firstName = t.string;
|
|
166
|
+
lastName = t.string;
|
|
167
|
+
age = t.number;
|
|
168
|
+
|
|
169
|
+
get fullName() {
|
|
170
|
+
return `${this.firstName} ${this.lastName}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
greet() {
|
|
174
|
+
return `Hello, ${this.name}!`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
isAdult() {
|
|
178
|
+
return this.age >= 18;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Methods work on entity proxies just like regular methods:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
const user = result.user;
|
|
187
|
+
user.fullName; // "Alice Smith"
|
|
188
|
+
user.greet(); // "Hello, Alice!"
|
|
189
|
+
user.isAdult(); // true
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Entity Cache Configuration
|
|
195
|
+
|
|
196
|
+
You can control how long unused entities stay in memory using the static `cache` property on the entity class. The `gcTime` option specifies the number of **minutes** an entity remains in the cache after it is no longer referenced by any active query.
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
class User extends Entity {
|
|
200
|
+
static cache = { gcTime: 5 }; // Keep in cache for 5 minutes after last use
|
|
201
|
+
|
|
202
|
+
__typename = t.typename('User');
|
|
203
|
+
id = t.id;
|
|
204
|
+
|
|
205
|
+
name = t.string;
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
| `gcTime` value | Behavior |
|
|
210
|
+
| --------------------- | -------------------------------------------------------------------- |
|
|
211
|
+
| `undefined` (default) | Entity is evicted immediately when no queries reference it |
|
|
212
|
+
| `0` | Entity is evicted on the next tick |
|
|
213
|
+
| `5` | Entity stays in cache for 5 minutes after last reference is released |
|
|
214
|
+
| `Infinity` | Entity is never garbage collected |
|
|
215
|
+
|
|
216
|
+
{% callout %}
|
|
217
|
+
Cache configuration is set at the entity class level, not per-query. All instances of `User` share the same GC policy.
|
|
218
|
+
{% /callout %}
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Deduplication in Practice
|
|
223
|
+
|
|
224
|
+
One of the most powerful features of Fetchium's entity system is automatic deduplication. Here is a concrete example showing how it works across multiple queries.
|
|
225
|
+
|
|
226
|
+
Consider a social feed where you fetch a list of posts and also fetch individual user profiles:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
class User extends Entity {
|
|
230
|
+
__typename = t.typename('User');
|
|
231
|
+
id = t.id;
|
|
232
|
+
|
|
233
|
+
name = t.string;
|
|
234
|
+
avatar = t.string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
class Post extends Entity {
|
|
238
|
+
__typename = t.typename('Post');
|
|
239
|
+
id = t.id;
|
|
240
|
+
|
|
241
|
+
title = t.string;
|
|
242
|
+
author = t.entity(User);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
class GetFeed extends RESTQuery {
|
|
246
|
+
path = '/feed';
|
|
247
|
+
|
|
248
|
+
result = { posts: t.array(t.entity(Post)) };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
class GetUser extends RESTQuery {
|
|
252
|
+
params = { id: t.id };
|
|
253
|
+
|
|
254
|
+
path = `/users/${this.params.id}`;
|
|
255
|
+
|
|
256
|
+
result = { user: t.entity(User) };
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
```tsx {% mode="react" %}
|
|
261
|
+
function Feed() {
|
|
262
|
+
const result = useQuery(GetFeed);
|
|
263
|
+
|
|
264
|
+
if (!result.isReady) return <div>Loading...</div>;
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div>
|
|
268
|
+
{result.value.posts.map((post) => (
|
|
269
|
+
<PostCard key={post.id} post={post} />
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function UserProfile() {
|
|
276
|
+
const result = useQuery(GetUser, { id: '1' });
|
|
277
|
+
|
|
278
|
+
if (!result.isReady) return <div>Loading...</div>;
|
|
279
|
+
|
|
280
|
+
// If user #1 also authored a post in the feed, this is the SAME proxy.
|
|
281
|
+
// Updating the user's name here updates it in the feed too.
|
|
282
|
+
return <h1>{result.value.user.name}</h1>;
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
```tsx {% mode="signalium" %}
|
|
287
|
+
const Feed = component(() => {
|
|
288
|
+
const { posts } = fetchQuery(GetFeed);
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<div>
|
|
292
|
+
{posts.map((post) => (
|
|
293
|
+
<PostCard key={post.id} post={post} />
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const UserProfile = component(() => {
|
|
300
|
+
const { user } = fetchQuery(GetUser, { id: '1' });
|
|
301
|
+
|
|
302
|
+
// If user #1 also authored a post in the feed, this is the SAME proxy.
|
|
303
|
+
// Updating the user's name here updates it in the feed too.
|
|
304
|
+
return <h1>{user.name}</h1>;
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
If the feed response includes posts authored by User #1, and you also fetch User #1 directly via `GetUser`, both queries share the same `User` proxy. A mutation that updates User #1's name will be reflected in both the feed and the profile -- with no manual cache invalidation needed.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Subscriptions
|
|
313
|
+
|
|
314
|
+
Entities can subscribe to real-time updates by defining a `__subscribe` method. When an entity proxy is actively being read by a reactive context (a component, a watcher, etc.), Fetchium will call `__subscribe` to establish a real-time connection.
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
class User extends Entity {
|
|
318
|
+
__typename = t.typename('User');
|
|
319
|
+
id = t.id;
|
|
320
|
+
|
|
321
|
+
name = t.string;
|
|
322
|
+
email = t.string;
|
|
323
|
+
|
|
324
|
+
__subscribe(onEvent) {
|
|
325
|
+
// Connect to a WebSocket, SSE stream, or other real-time source
|
|
326
|
+
const ws = new WebSocket(`/ws/users/${this.id}`);
|
|
327
|
+
|
|
328
|
+
ws.onmessage = (msg) => {
|
|
329
|
+
const data = JSON.parse(msg.data);
|
|
330
|
+
onEvent({
|
|
331
|
+
type: 'update',
|
|
332
|
+
typename: 'User',
|
|
333
|
+
data: { id: this.id, ...data },
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// Return a cleanup function
|
|
338
|
+
return () => ws.close();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
The `__subscribe` method receives an `onEvent` callback. Call it with a mutation event whenever the entity changes. Fetchium will merge the update into the entity store, and all proxies will reflect the new data.
|
|
344
|
+
|
|
345
|
+
The cleanup function returned from `__subscribe` is called when the entity is no longer being actively observed (i.e., no components or watchers are reading it).
|
|
346
|
+
|
|
347
|
+
{% callout %}
|
|
348
|
+
The `__subscribe` method is only called when the entity is being actively consumed in a reactive context. If no component or reactive function is reading the entity's properties, the subscription will not be established (or will be torn down if it was previously active).
|
|
349
|
+
{% /callout %}
|
|
350
|
+
|
|
351
|
+
For more details on real-time streaming patterns, see the [Streaming guide](/core/streaming).
|