interaqt 0.3.0 → 0.3.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/agent/.claude/agents/code-generation-handler.md +2 -0
- package/agent/.claude/agents/computation-generation-handler.md +1 -0
- package/agent/.claude/agents/implement-design-handler.md +4 -13
- package/agent/.claude/agents/requirements-analysis-handler.md +46 -14
- package/agent/agentspace/knowledge/generator/api-reference.md +3378 -0
- package/agent/agentspace/knowledge/generator/basic-interaction-generation.md +377 -0
- package/agent/agentspace/knowledge/generator/computation-analysis.md +307 -0
- package/agent/agentspace/knowledge/generator/computation-implementation.md +959 -0
- package/agent/agentspace/knowledge/generator/data-analysis.md +463 -0
- package/agent/agentspace/knowledge/generator/entity-relation-generation.md +395 -0
- package/agent/agentspace/knowledge/generator/permission-implementation.md +460 -0
- package/agent/agentspace/knowledge/generator/permission-test-implementation.md +870 -0
- package/agent/agentspace/knowledge/generator/test-implementation.md +674 -0
- package/agent/agentspace/knowledge/usage/00-mindset-shift.md +322 -0
- package/agent/agentspace/knowledge/usage/01-core-concepts.md +131 -0
- package/agent/agentspace/knowledge/usage/02-define-entities-properties.md +407 -0
- package/agent/agentspace/knowledge/usage/03-entity-relations.md +599 -0
- package/agent/agentspace/knowledge/usage/04-reactive-computations.md +2186 -0
- package/agent/agentspace/knowledge/usage/05-interactions.md +1411 -0
- package/agent/agentspace/knowledge/usage/06-attributive-permissions.md +10 -0
- package/agent/agentspace/knowledge/usage/07-payload-parameters.md +593 -0
- package/agent/agentspace/knowledge/usage/08-activities.md +863 -0
- package/agent/agentspace/knowledge/usage/09-filtered-entities.md +784 -0
- package/agent/agentspace/knowledge/usage/10-async-computations.md +734 -0
- package/agent/agentspace/knowledge/usage/11-global-dictionaries.md +942 -0
- package/agent/agentspace/knowledge/usage/12-data-querying.md +1033 -0
- package/agent/agentspace/knowledge/usage/13-testing.md +1201 -0
- package/agent/agentspace/knowledge/usage/14-api-reference.md +1606 -0
- package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +1122 -0
- package/agent/agentspace/knowledge/usage/16-frontend-page-design-guide.md +485 -0
- package/agent/agentspace/knowledge/usage/17-performance-optimization.md +283 -0
- package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +176 -0
- package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +563 -0
- package/agent/agentspace/knowledge/usage/README.md +148 -0
- package/package.json +1 -1
|
@@ -0,0 +1,2186 @@
|
|
|
1
|
+
# How to Use Reactive Computations
|
|
2
|
+
|
|
3
|
+
⚠️ **Prerequisite: Please read [00-mindset-shift.md](./00-mindset-shift.md) first to understand declarative thinking**
|
|
4
|
+
|
|
5
|
+
Reactive computation is the core feature of the interaqt framework. Its essence is **declaring what data is**, rather than specifying how to compute data.
|
|
6
|
+
|
|
7
|
+
## ⚠️ IMPORTANT: Correct Usage of Computations
|
|
8
|
+
|
|
9
|
+
Computations (such as Count, Transform, WeightedSummation, etc.) **MUST and ONLY** be placed in the `computation` field of Entity, Relation, or Property definitions.
|
|
10
|
+
|
|
11
|
+
❌ **WRONG**: Declaring computations separately and passing them to Controller
|
|
12
|
+
```javascript
|
|
13
|
+
// Wrong: Separately declaring computations
|
|
14
|
+
const UserCreationTransform = Transform.create({...})
|
|
15
|
+
const computations = [UserCreationTransform, ...]
|
|
16
|
+
|
|
17
|
+
// Wrong: Passing to Controller
|
|
18
|
+
const controller = new Controller({
|
|
19
|
+
|
|
20
|
+
system: system,
|
|
21
|
+
|
|
22
|
+
entities: entities,
|
|
23
|
+
|
|
24
|
+
relations: relations,
|
|
25
|
+
|
|
26
|
+
activities: [],
|
|
27
|
+
|
|
28
|
+
interactions: interactions,
|
|
29
|
+
|
|
30
|
+
dict: computations,
|
|
31
|
+
|
|
32
|
+
recordMutationSideEffects: []
|
|
33
|
+
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
✅ **CORRECT**: Using computations in the computation field
|
|
38
|
+
```javascript
|
|
39
|
+
// Correct: Using computation in Property definition
|
|
40
|
+
Property.create({
|
|
41
|
+
name: 'userCount',
|
|
42
|
+
type: 'number',
|
|
43
|
+
defaultValue: () => 0,
|
|
44
|
+
computation: Count.create({
|
|
45
|
+
record: User
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Note**: Controller does NOT accept a computations parameter. All computations should be defined within the `computation` field of Entity/Relation/Property definitions.
|
|
51
|
+
|
|
52
|
+
## Core Mindset: What Data "Is", Not "How to Compute"
|
|
53
|
+
|
|
54
|
+
### ❌ Wrong Mindset: Trying to Compute Data
|
|
55
|
+
```javascript
|
|
56
|
+
// Wrong: Trying to write "how to compute" logic
|
|
57
|
+
function updateLikeCount(postId) {
|
|
58
|
+
const likes = db.query('SELECT COUNT(*) FROM likes WHERE postId = ?', postId);
|
|
59
|
+
db.update('posts', { likeCount: likes }, { id: postId });
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### ✅ Correct Mindset: Declare What Data Is
|
|
64
|
+
```javascript
|
|
65
|
+
// Correct: Declare that like count "is" the count of like relations
|
|
66
|
+
Property.create({
|
|
67
|
+
name: 'likeCount',
|
|
68
|
+
computation: Count.create({
|
|
69
|
+
record: LikeRelation // Like count is the Count of like relations
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Basic Concepts of Reactive Computation
|
|
75
|
+
|
|
76
|
+
### What is Reactive Computation
|
|
77
|
+
|
|
78
|
+
Reactive computation is a **declarative way of defining data**:
|
|
79
|
+
- **Declarative**: You declare what data "is", not "how to compute"
|
|
80
|
+
- **Automatically maintained**: When dependent data changes, computed results update automatically
|
|
81
|
+
- **Incremental computation**: Framework uses efficient incremental algorithms to avoid unnecessary recomputation
|
|
82
|
+
- **Persistent**: Computation results are stored in the database for fast queries
|
|
83
|
+
|
|
84
|
+
### Core Principle: Data Existence
|
|
85
|
+
|
|
86
|
+
In interaqt, all data has its "reason for existence":
|
|
87
|
+
- User post count **exists** because it is the Count of user-post relations
|
|
88
|
+
- Order total amount **exists** because it is the weighted sum of order items
|
|
89
|
+
- Product inventory **exists** because it is initial inventory minus sales quantity
|
|
90
|
+
- Notification records **exist** because they are Transform results of specific interaction events
|
|
91
|
+
|
|
92
|
+
### Reactive Computation vs Regular Computed Properties
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
// Regular computed property: Recalculates every time it's queried
|
|
96
|
+
const Post = Entity.create({
|
|
97
|
+
name: 'Post',
|
|
98
|
+
properties: [
|
|
99
|
+
Property.create({
|
|
100
|
+
name: 'likeCount',
|
|
101
|
+
type: 'number',
|
|
102
|
+
getValue: async (record) => {
|
|
103
|
+
// Database query executed every time accessed
|
|
104
|
+
return await controller.count('Like', { post: record.id });
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
]
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Reactive computation: Results are cached, only updated when data changes
|
|
111
|
+
const Post = Entity.create({
|
|
112
|
+
name: 'Post',
|
|
113
|
+
properties: [
|
|
114
|
+
Property.create({
|
|
115
|
+
name: 'likeCount',
|
|
116
|
+
type: 'number',
|
|
117
|
+
defaultValue: () => 0,
|
|
118
|
+
computation: Count.create({
|
|
119
|
+
record: likeRelation // Pass relation instance, not entity
|
|
120
|
+
}) // Automatically maintained, high performance
|
|
121
|
+
})
|
|
122
|
+
]
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Using Count for Counting
|
|
127
|
+
|
|
128
|
+
Count is the most commonly used reactive computation type for counting relations or entities.
|
|
129
|
+
|
|
130
|
+
### Basic Usage
|
|
131
|
+
|
|
132
|
+
```javascript
|
|
133
|
+
import { Entity, Property, Relation, Count } from 'interaqt';
|
|
134
|
+
|
|
135
|
+
// Define entities and relations
|
|
136
|
+
const User = Entity.create({
|
|
137
|
+
name: 'User',
|
|
138
|
+
properties: [
|
|
139
|
+
Property.create({ name: 'name', type: 'string' })
|
|
140
|
+
]
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const Post = Entity.create({
|
|
144
|
+
name: 'Post',
|
|
145
|
+
properties: [
|
|
146
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
147
|
+
Property.create({ name: 'content', type: 'string' }),
|
|
148
|
+
// Use Count to calculate like count
|
|
149
|
+
Property.create({
|
|
150
|
+
name: 'likeCount',
|
|
151
|
+
type: 'number',
|
|
152
|
+
defaultValue: () => 0,
|
|
153
|
+
computation: Count.create({
|
|
154
|
+
record: Like
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
]
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const Like = Relation.create({
|
|
161
|
+
source: User,
|
|
162
|
+
sourceProperty: 'likedPosts',
|
|
163
|
+
target: Post,
|
|
164
|
+
targetProperty: 'likers',
|
|
165
|
+
type: 'n:n'
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Count with Filter Conditions
|
|
170
|
+
|
|
171
|
+
Count supports using callback functions to filter records:
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
const Post = Entity.create({
|
|
175
|
+
name: 'Post',
|
|
176
|
+
properties: [
|
|
177
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
178
|
+
Property.create({ name: 'status', type: 'string' })
|
|
179
|
+
]
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Count published posts for a user
|
|
183
|
+
const User = Entity.create({
|
|
184
|
+
name: 'User',
|
|
185
|
+
properties: [
|
|
186
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
187
|
+
Property.create({
|
|
188
|
+
name: 'publishedPostCount',
|
|
189
|
+
type: 'number',
|
|
190
|
+
defaultValue: () => 0,
|
|
191
|
+
computation: Count.create({
|
|
192
|
+
record: UserPostRelation,
|
|
193
|
+
attributeQuery: [['target', {attributeQuery: ['status']}]],
|
|
194
|
+
callback: function(relation) {
|
|
195
|
+
return relation.target.status === 'published'
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
]
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const UserPostRelation = Relation.create({
|
|
203
|
+
source: User,
|
|
204
|
+
sourceProperty: 'posts',
|
|
205
|
+
target: Post,
|
|
206
|
+
targetProperty: 'author',
|
|
207
|
+
type: '1:n'
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Dynamic Filtering Based on Data Dependencies
|
|
212
|
+
|
|
213
|
+
Count supports dataDeps parameter for dynamic filtering based on global data or other data sources:
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
// Count high-score posts based on global score threshold
|
|
217
|
+
const User = Entity.create({
|
|
218
|
+
name: 'User',
|
|
219
|
+
properties: [
|
|
220
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
221
|
+
Property.create({
|
|
222
|
+
name: 'highScorePostCount',
|
|
223
|
+
type: 'number',
|
|
224
|
+
defaultValue: () => 0,
|
|
225
|
+
computation: Count.create({
|
|
226
|
+
record: UserPostRelation,
|
|
227
|
+
attributeQuery: [['target', {attributeQuery: ['score']}]],
|
|
228
|
+
dataDeps: {
|
|
229
|
+
scoreThreshold: {
|
|
230
|
+
type: 'global',
|
|
231
|
+
source: Dictionary.create({
|
|
232
|
+
name: 'highScoreThreshold',
|
|
233
|
+
type: 'number',
|
|
234
|
+
collection: false
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
callback: function(relation, dataDeps) {
|
|
239
|
+
return relation.target.score >= dataDeps.scoreThreshold
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
]
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Global active user count based on global active days setting
|
|
247
|
+
const activeUsersCount = Dictionary.create({
|
|
248
|
+
name: 'activeUsersCount',
|
|
249
|
+
type: 'number',
|
|
250
|
+
collection: false,
|
|
251
|
+
computation: Count.create({
|
|
252
|
+
record: User,
|
|
253
|
+
attributeQuery: ['lastLoginDate'],
|
|
254
|
+
dataDeps: {
|
|
255
|
+
activeDays: {
|
|
256
|
+
type: 'global',
|
|
257
|
+
source: Dictionary.create({
|
|
258
|
+
name: 'userActiveDays',
|
|
259
|
+
type: 'number',
|
|
260
|
+
collection: false
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
callback: function(user, dataDeps) {
|
|
265
|
+
const daysSinceLogin = (Date.now() - new Date(user.lastLoginDate).getTime()) / (1000 * 60 * 60 * 24)
|
|
266
|
+
return daysSinceLogin <= dataDeps.activeDays
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
});
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Relation Direction Control
|
|
273
|
+
|
|
274
|
+
For relation counting, use the direction parameter to specify counting direction:
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
const User = Entity.create({
|
|
278
|
+
name: 'User',
|
|
279
|
+
properties: [
|
|
280
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
281
|
+
// Count posts authored by user
|
|
282
|
+
Property.create({
|
|
283
|
+
name: 'authoredPostCount',
|
|
284
|
+
type: 'number',
|
|
285
|
+
defaultValue: () => 0,
|
|
286
|
+
computation: Count.create({
|
|
287
|
+
record: UserPostRelation,
|
|
288
|
+
direction: 'target' // From user perspective to posts
|
|
289
|
+
})
|
|
290
|
+
}),
|
|
291
|
+
// Count following relationships as follower
|
|
292
|
+
Property.create({
|
|
293
|
+
name: 'followingCount',
|
|
294
|
+
type: 'number',
|
|
295
|
+
defaultValue: () => 0,
|
|
296
|
+
computation: Count.create({
|
|
297
|
+
record: FollowRelation,
|
|
298
|
+
direction: 'target' // From user perspective to followed users
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
]
|
|
302
|
+
});
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Attribute Query Optimization
|
|
306
|
+
|
|
307
|
+
Use attributeQuery parameter to optimize data fetching, only querying attributes needed for computation:
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
const User = Entity.create({
|
|
311
|
+
name: 'User',
|
|
312
|
+
properties: [
|
|
313
|
+
Property.create({
|
|
314
|
+
name: 'completedTaskCount',
|
|
315
|
+
type: 'number',
|
|
316
|
+
defaultValue: () => 0,
|
|
317
|
+
computation: Count.create({
|
|
318
|
+
record: UserTaskRelation,
|
|
319
|
+
attributeQuery: [['target', {attributeQuery: ['status', 'completedAt']}]],
|
|
320
|
+
callback: function(relation) {
|
|
321
|
+
const task = relation.target
|
|
322
|
+
return task.status === 'completed' && task.completedAt !== null
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
})
|
|
326
|
+
]
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Real-time Update Mechanism
|
|
331
|
+
|
|
332
|
+
When related data changes, Count automatically updates:
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
// When user likes a post
|
|
336
|
+
const likePost = async (userId, postId) => {
|
|
337
|
+
// Create like relation
|
|
338
|
+
await controller.createRelation('Like', {
|
|
339
|
+
source: userId,
|
|
340
|
+
target: postId
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// likeCount will automatically +1, no manual update needed
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// When user unlikes a post
|
|
347
|
+
const unlikePost = async (userId, postId) => {
|
|
348
|
+
// Remove like relation
|
|
349
|
+
await controller.removeRelation('Like', {
|
|
350
|
+
source: userId,
|
|
351
|
+
target: postId
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// likeCount will automatically -1
|
|
355
|
+
};
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Using WeightedSummation for Weighted Sums
|
|
359
|
+
|
|
360
|
+
WeightedSummation is used to calculate weighted totals, commonly used for calculating total scores, total prices, etc.
|
|
361
|
+
|
|
362
|
+
### Basic Usage
|
|
363
|
+
|
|
364
|
+
```javascript
|
|
365
|
+
// Order item entity
|
|
366
|
+
const OrderItem = Entity.create({
|
|
367
|
+
name: 'OrderItem',
|
|
368
|
+
properties: [
|
|
369
|
+
Property.create({ name: 'quantity', type: 'number' }),
|
|
370
|
+
Property.create({ name: 'price', type: 'number' }),
|
|
371
|
+
Property.create({
|
|
372
|
+
name: 'subtotal',
|
|
373
|
+
type: 'number',
|
|
374
|
+
getValue: (record) => record.quantity * record.price
|
|
375
|
+
})
|
|
376
|
+
]
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Order entity
|
|
380
|
+
const Order = Entity.create({
|
|
381
|
+
name: 'Order',
|
|
382
|
+
properties: [
|
|
383
|
+
Property.create({ name: 'orderNumber', type: 'string' }),
|
|
384
|
+
// Calculate order total amount
|
|
385
|
+
Property.create({
|
|
386
|
+
name: 'totalAmount',
|
|
387
|
+
type: 'number',
|
|
388
|
+
defaultValue: () => 0,
|
|
389
|
+
computation: WeightedSummation.create({
|
|
390
|
+
record: OrderItems,
|
|
391
|
+
attributeQuery: [['target', { attributeQuery: ['quantity', 'price'] }]],
|
|
392
|
+
callback: (relation) => ({
|
|
393
|
+
weight: 1,
|
|
394
|
+
value: relation.target.quantity * relation.target.price
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
]
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const OrderItems = Relation.create({
|
|
402
|
+
source: Order,
|
|
403
|
+
sourceProperty: 'items',
|
|
404
|
+
target: OrderItem,
|
|
405
|
+
targetProperty: 'order',
|
|
406
|
+
type: '1:n'
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Defining Weight Functions
|
|
411
|
+
|
|
412
|
+
Use functions to define more complex weight calculations:
|
|
413
|
+
|
|
414
|
+
```javascript
|
|
415
|
+
// Student grade entity
|
|
416
|
+
const Grade = Entity.create({
|
|
417
|
+
name: 'Grade',
|
|
418
|
+
properties: [
|
|
419
|
+
Property.create({ name: 'subject', type: 'string' }),
|
|
420
|
+
Property.create({ name: 'score', type: 'number' }),
|
|
421
|
+
Property.create({ name: 'credit', type: 'number' }) // Credits
|
|
422
|
+
]
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Student entity
|
|
426
|
+
const Student = Entity.create({
|
|
427
|
+
name: 'Student',
|
|
428
|
+
properties: [
|
|
429
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
430
|
+
// Calculate weighted average score (GPA)
|
|
431
|
+
Property.create({
|
|
432
|
+
name: 'gpa',
|
|
433
|
+
type: 'number',
|
|
434
|
+
defaultValue: () => 0,
|
|
435
|
+
computation: WeightedSummation.create({
|
|
436
|
+
record: StudentGrades,
|
|
437
|
+
callback: (relation) => ({
|
|
438
|
+
weight: relation.target.credit,
|
|
439
|
+
value: relation.target.score
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
}),
|
|
443
|
+
// Calculate total credits
|
|
444
|
+
Property.create({
|
|
445
|
+
name: 'totalCredits',
|
|
446
|
+
type: 'number',
|
|
447
|
+
defaultValue: () => 0,
|
|
448
|
+
computation: WeightedSummation.create({
|
|
449
|
+
record: StudentGrades,
|
|
450
|
+
callback: (relation) => ({
|
|
451
|
+
weight: 1,
|
|
452
|
+
value: relation.target.credit
|
|
453
|
+
})
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
]
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### Conditional Summation
|
|
461
|
+
|
|
462
|
+
Add conditions to only sum records that meet specific criteria:
|
|
463
|
+
|
|
464
|
+
```javascript
|
|
465
|
+
const Student = Entity.create({
|
|
466
|
+
name: 'Student',
|
|
467
|
+
properties: [
|
|
468
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
469
|
+
// Only count credits for passed subjects
|
|
470
|
+
Property.create({
|
|
471
|
+
name: 'passedCredits',
|
|
472
|
+
type: 'number',
|
|
473
|
+
defaultValue: () => 0,
|
|
474
|
+
computation: WeightedSummation.create({
|
|
475
|
+
record: StudentGrades,
|
|
476
|
+
callback: (relation) => {
|
|
477
|
+
// Only count subjects with score >= 60
|
|
478
|
+
if (relation.target.score >= 60) {
|
|
479
|
+
return { weight: 1, value: relation.target.credit }
|
|
480
|
+
}
|
|
481
|
+
return { weight: 0, value: 0 }
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
]
|
|
486
|
+
});
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Using Every and Any for Conditional Checks
|
|
490
|
+
|
|
491
|
+
Every and Any are used to check whether elements in a collection meet specific conditions.
|
|
492
|
+
|
|
493
|
+
### Every: Check All Elements Meet Condition
|
|
494
|
+
|
|
495
|
+
```javascript
|
|
496
|
+
const Task = Entity.create({
|
|
497
|
+
name: 'Task',
|
|
498
|
+
properties: [
|
|
499
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
500
|
+
Property.create({ name: 'status', type: 'string' }) // pending, completed
|
|
501
|
+
]
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const Project = Entity.create({
|
|
505
|
+
name: 'Project',
|
|
506
|
+
properties: [
|
|
507
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
508
|
+
// Check if all tasks are completed
|
|
509
|
+
Property.create({
|
|
510
|
+
name: 'isCompleted',
|
|
511
|
+
type: 'boolean',
|
|
512
|
+
defaultValue: () => false,
|
|
513
|
+
computation: Every.create({
|
|
514
|
+
record: ProjectTasks,
|
|
515
|
+
callback: (relation) => relation.target.status === 'completed'
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
]
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
const ProjectTasks = Relation.create({
|
|
522
|
+
source: Project,
|
|
523
|
+
sourceProperty: 'tasks',
|
|
524
|
+
target: Task,
|
|
525
|
+
targetProperty: 'project',
|
|
526
|
+
type: '1:n'
|
|
527
|
+
});
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Any: Check Any Element Meets Condition
|
|
531
|
+
|
|
532
|
+
```javascript
|
|
533
|
+
const User = Entity.create({
|
|
534
|
+
name: 'User',
|
|
535
|
+
properties: [
|
|
536
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
537
|
+
Property.create({ name: 'role', type: 'string' })
|
|
538
|
+
]
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const Project = Entity.create({
|
|
542
|
+
name: 'Project',
|
|
543
|
+
properties: [
|
|
544
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
545
|
+
// Check if project has admin
|
|
546
|
+
Property.create({
|
|
547
|
+
name: 'hasAdmin',
|
|
548
|
+
type: 'boolean',
|
|
549
|
+
defaultValue: () => false,
|
|
550
|
+
computation: Any.create({
|
|
551
|
+
record: ProjectMember,
|
|
552
|
+
callback: (relation) => relation.role === 'admin'
|
|
553
|
+
})
|
|
554
|
+
})
|
|
555
|
+
]
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const ProjectMember = Relation.create({
|
|
559
|
+
source: Project,
|
|
560
|
+
sourceProperty: 'members',
|
|
561
|
+
target: User,
|
|
562
|
+
targetProperty: 'projects',
|
|
563
|
+
type: 'n:n',
|
|
564
|
+
properties: [
|
|
565
|
+
Property.create({ name: 'role', type: 'string', defaultValue: () => 'member' })
|
|
566
|
+
]
|
|
567
|
+
});
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Complex Conditional Checks
|
|
571
|
+
|
|
572
|
+
Use more complex conditional expressions:
|
|
573
|
+
|
|
574
|
+
```javascript
|
|
575
|
+
const Order = Entity.create({
|
|
576
|
+
name: 'Order',
|
|
577
|
+
properties: [
|
|
578
|
+
Property.create({ name: 'status', type: 'string' }),
|
|
579
|
+
// Check if all order items are in stock
|
|
580
|
+
Property.create({
|
|
581
|
+
name: 'allItemsInStock',
|
|
582
|
+
type: 'boolean',
|
|
583
|
+
defaultValue: () => false,
|
|
584
|
+
computation: Every.create({
|
|
585
|
+
record: OrderItems,
|
|
586
|
+
callback: (relation) => {
|
|
587
|
+
const item = relation.target;
|
|
588
|
+
return item.quantity > 0 && item.stockQuantity >= item.quantity;
|
|
589
|
+
}
|
|
590
|
+
})
|
|
591
|
+
}),
|
|
592
|
+
// Check if any high-value items exist
|
|
593
|
+
Property.create({
|
|
594
|
+
name: 'hasHighValueItem',
|
|
595
|
+
type: 'boolean',
|
|
596
|
+
defaultValue: () => false,
|
|
597
|
+
computation: Any.create({
|
|
598
|
+
record: OrderItems,
|
|
599
|
+
callback: (relation) => {
|
|
600
|
+
const item = relation.target;
|
|
601
|
+
return (item.quantity * item.price) > 1000;
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
})
|
|
605
|
+
]
|
|
606
|
+
});
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## Using Transform for Data Transformation
|
|
610
|
+
|
|
611
|
+
Transform is the most flexible reactive computation type, allowing you to define custom transformation logic.
|
|
612
|
+
|
|
613
|
+
### Understanding Transform's Essence
|
|
614
|
+
|
|
615
|
+
Transform is fundamentally about **transforming data from one collection to another collection**. It's a declarative way to express how one set of data transforms into another set of data. Common examples include:
|
|
616
|
+
|
|
617
|
+
- Transforming `InteractionEventEntity` data into specific entity data (e.g., user interactions → entities)
|
|
618
|
+
- Transforming `InteractionEventEntity` data into relation data (e.g., follow action → user follow relation)
|
|
619
|
+
- Transforming one entity type into another entity type (e.g., Product → DiscountedProduct)
|
|
620
|
+
- Transforming relation data into derived entity data
|
|
621
|
+
|
|
622
|
+
**Important**: Transform **cannot** be used to express property computations within the same entity. For property-level computations that depend only on the current record's data, use `getValue` instead. Transform is about inter-collection transformations, not intra-record calculations.
|
|
623
|
+
|
|
624
|
+
### ⚠️ CRITICAL: When to Use Transform vs getValue
|
|
625
|
+
|
|
626
|
+
**Transform** is designed for creating **derived entities** from other entities or relations:
|
|
627
|
+
- ✅ Use Transform when creating a new entity type based on data from another entity
|
|
628
|
+
- ✅ Use Transform when transforming relation data into entity data
|
|
629
|
+
- ✅ Use Transform when the source data comes from InteractionEventEntity
|
|
630
|
+
|
|
631
|
+
**getValue** is for computed properties within the same entity:
|
|
632
|
+
- ✅ Use getValue for simple computed properties (like fullName from firstName + lastName)
|
|
633
|
+
- ✅ Use getValue when the computation only needs data from the current record
|
|
634
|
+
|
|
635
|
+
❌ **NEVER** use Transform with `record` pointing to the entity being defined - this creates a circular reference!
|
|
636
|
+
|
|
637
|
+
### Basic Usage
|
|
638
|
+
|
|
639
|
+
```javascript
|
|
640
|
+
// For simple property transformations within the same entity, use getValue instead of Transform
|
|
641
|
+
const User = Entity.create({
|
|
642
|
+
name: 'User',
|
|
643
|
+
properties: [
|
|
644
|
+
Property.create({ name: 'firstName', type: 'string' }),
|
|
645
|
+
Property.create({ name: 'lastName', type: 'string' }),
|
|
646
|
+
// ✅ Correct: Use getValue for computed properties within the same entity
|
|
647
|
+
Property.create({
|
|
648
|
+
name: 'fullName',
|
|
649
|
+
type: 'string',
|
|
650
|
+
getValue: (record) => `${record.firstName} ${record.lastName}`
|
|
651
|
+
})
|
|
652
|
+
]
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// ⚠️ IMPORTANT: Transform should NOT reference the entity being defined
|
|
656
|
+
// Transform is meant for creating derived entities from other entities or relations
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Correct Transform Usage Example
|
|
660
|
+
|
|
661
|
+
```javascript
|
|
662
|
+
// ✅ Correct: Create a derived entity based on another entity
|
|
663
|
+
const Product = Entity.create({
|
|
664
|
+
name: 'Product',
|
|
665
|
+
properties: [
|
|
666
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
667
|
+
Property.create({ name: 'price', type: 'number' }),
|
|
668
|
+
Property.create({ name: 'isAvailable', type: 'boolean' })
|
|
669
|
+
]
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// Transform creates a new entity type from existing Product data
|
|
673
|
+
const DiscountedProduct = Entity.create({
|
|
674
|
+
name: 'DiscountedProduct',
|
|
675
|
+
properties: [
|
|
676
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
677
|
+
Property.create({ name: 'originalPrice', type: 'number' }),
|
|
678
|
+
Property.create({ name: 'discountedPrice', type: 'number' }),
|
|
679
|
+
Property.create({ name: 'discount', type: 'string' })
|
|
680
|
+
],
|
|
681
|
+
computation: Transform.create({
|
|
682
|
+
record: Product, // References a different, already-defined entity
|
|
683
|
+
callback: (product) => {
|
|
684
|
+
return {
|
|
685
|
+
name: product.name,
|
|
686
|
+
originalPrice: product.price,
|
|
687
|
+
discountedPrice: product.price * 0.9,
|
|
688
|
+
discount: '10%'
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
})
|
|
692
|
+
});
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
### Transform Based on Related Data
|
|
696
|
+
|
|
697
|
+
```javascript
|
|
698
|
+
const User = Entity.create({
|
|
699
|
+
name: 'User',
|
|
700
|
+
properties: [
|
|
701
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
702
|
+
// Generate user tag summary
|
|
703
|
+
Property.create({
|
|
704
|
+
name: 'tagSummary',
|
|
705
|
+
type: 'string',
|
|
706
|
+
defaultValue: () => '',
|
|
707
|
+
computed: function(user) {
|
|
708
|
+
// Assuming user has a tags property that's an array
|
|
709
|
+
const tags = user.tags || [];
|
|
710
|
+
if (tags.length === 0) return 'No tags';
|
|
711
|
+
if (tags.length <= 3) return tags.map(t => t.name).join(', ');
|
|
712
|
+
return `${tags.slice(0, 3).map(t => t.name).join(', ')} and ${tags.length - 3} more`;
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
]
|
|
716
|
+
});
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
### Aggregation Computation
|
|
720
|
+
|
|
721
|
+
Transform can be used for complex aggregation calculations:
|
|
722
|
+
|
|
723
|
+
```javascript
|
|
724
|
+
const User = Entity.create({
|
|
725
|
+
name: 'User',
|
|
726
|
+
properties: [
|
|
727
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
728
|
+
// Calculate user activity statistics
|
|
729
|
+
Property.create({
|
|
730
|
+
name: 'activityStats',
|
|
731
|
+
type: 'object',
|
|
732
|
+
defaultValue: () => ({}),
|
|
733
|
+
computed: function(user) {
|
|
734
|
+
// Assuming user has posts property that's an array
|
|
735
|
+
const posts = user.posts || [];
|
|
736
|
+
const now = new Date();
|
|
737
|
+
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
738
|
+
const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
739
|
+
|
|
740
|
+
const recentPosts = posts.filter(p => new Date(p.createdAt) > oneWeekAgo);
|
|
741
|
+
const monthlyPosts = posts.filter(p => new Date(p.createdAt) > oneMonthAgo);
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
totalPosts: posts.length,
|
|
745
|
+
recentPosts: recentPosts.length,
|
|
746
|
+
monthlyPosts: monthlyPosts.length,
|
|
747
|
+
averageLikes: posts.reduce((sum, p) => sum + (p.likeCount || 0), 0) / posts.length || 0
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
})
|
|
751
|
+
]
|
|
752
|
+
});
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Data Format Transformation
|
|
756
|
+
|
|
757
|
+
```javascript
|
|
758
|
+
const Product = Entity.create({
|
|
759
|
+
name: 'Product',
|
|
760
|
+
properties: [
|
|
761
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
762
|
+
Property.create({ name: 'price', type: 'number' }),
|
|
763
|
+
Property.create({ name: 'currency', type: 'string', defaultValue: () => 'USD' }),
|
|
764
|
+
// ✅ Correct: Use getValue for simple property formatting
|
|
765
|
+
Property.create({
|
|
766
|
+
name: 'formattedPrice',
|
|
767
|
+
type: 'string',
|
|
768
|
+
getValue: (record) => {
|
|
769
|
+
const currencySymbols = {
|
|
770
|
+
'USD': '$',
|
|
771
|
+
'EUR': '€',
|
|
772
|
+
'GBP': '£',
|
|
773
|
+
'CNY': '¥'
|
|
774
|
+
};
|
|
775
|
+
const symbol = currencySymbols[record.currency] || record.currency;
|
|
776
|
+
return `${symbol}${record.price.toFixed(2)}`;
|
|
777
|
+
}
|
|
778
|
+
})
|
|
779
|
+
]
|
|
780
|
+
});
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
### Transform from Interaction Data: Core of Declarative Data Transformation
|
|
784
|
+
|
|
785
|
+
One of the most important use cases for Transform is **transforming from user interaction data to other business data**. This embodies the core philosophy of interaqt framework: **everything is data, data transforms from data**.
|
|
786
|
+
|
|
787
|
+
#### Core Concept: Interactions Are Data, Data Transforms from Data
|
|
788
|
+
|
|
789
|
+
In interaqt, user interactions (Interaction) are themselves data, stored in InteractionEventEntity. Transform is not the traditional "event-driven + callback" pattern, but **declarative data transformation relationships**:
|
|
790
|
+
|
|
791
|
+
> Declaration: DirectorMemo **is** the result of transforming InteractionEventEntity through certain transformation rules
|
|
792
|
+
|
|
793
|
+
This differs from traditional event-driven approaches:
|
|
794
|
+
- **Traditional event-driven**: When event occurs → Execute callback function → Manually create data
|
|
795
|
+
- **interaqt Transform**: Declare how one type of data transforms from another type of data
|
|
796
|
+
|
|
797
|
+
```typescript
|
|
798
|
+
// ❌ Wrong mindset: Imperatively create data manually in interaction handling
|
|
799
|
+
async function handleUserLogin(userId) {
|
|
800
|
+
await createLoginRecord(userId);
|
|
801
|
+
|
|
802
|
+
// Manual checking and creation - this is imperative "how to do"
|
|
803
|
+
const loginCount = await getLoginCountThisMonth(userId);
|
|
804
|
+
if (loginCount >= 10) {
|
|
805
|
+
await createActivityReward(userId, 'frequent_user');
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ✅ Correct mindset: Declaratively define data transformation relationships
|
|
810
|
+
// ActivityReward "is what": transformation result of qualifying InteractionEventEntity
|
|
811
|
+
const ActivityReward = Entity.create({
|
|
812
|
+
name: 'ActivityReward',
|
|
813
|
+
properties: [
|
|
814
|
+
Property.create({ name: 'type', type: 'string' }),
|
|
815
|
+
Property.create({ name: 'description', type: 'string' }),
|
|
816
|
+
Property.create({ name: 'createdAt', type: 'string' })
|
|
817
|
+
],
|
|
818
|
+
computation: Transform.create({
|
|
819
|
+
record: InteractionEventEntity,
|
|
820
|
+
attributeQuery: ['interactionName', 'user', 'createdAt'],
|
|
821
|
+
dataDeps: {
|
|
822
|
+
users: {
|
|
823
|
+
type: 'records',
|
|
824
|
+
source: User,
|
|
825
|
+
attributeQuery: ['username', 'monthlyLoginCount']
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
callback: (interactionEvents, dataDeps) => {
|
|
829
|
+
// Transform essence: define data transformation rules
|
|
830
|
+
// Input: InteractionEventEntity data + dependency data
|
|
831
|
+
// Output: ActivityReward data (or null)
|
|
832
|
+
|
|
833
|
+
return interactionEvents
|
|
834
|
+
.filter(event => event.interactionName === 'userLogin')
|
|
835
|
+
.map(event => {
|
|
836
|
+
const user = dataDeps.users.find(u => u.id === event.user.id);
|
|
837
|
+
|
|
838
|
+
// Declare transformation condition: when user monthly login count >= 10, this interaction data transforms to reward data
|
|
839
|
+
if (user && user.monthlyLoginCount >= 10) {
|
|
840
|
+
return {
|
|
841
|
+
type: 'frequent_user',
|
|
842
|
+
description: `${user.username} received active user reward`,
|
|
843
|
+
createdAt: event.createdAt,
|
|
844
|
+
userId: user.id
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Return null when transformation condition not met (this interaction doesn't produce reward data)
|
|
849
|
+
return null;
|
|
850
|
+
})
|
|
851
|
+
.filter(reward => reward !== null);
|
|
852
|
+
}
|
|
853
|
+
})
|
|
854
|
+
});
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
#### Transform's Conditional Transformation: null Return Mechanism
|
|
858
|
+
|
|
859
|
+
Transform supports returning `null` to indicate "some input data doesn't participate in transformation", which is the core mechanism for implementing conditional transformation:
|
|
860
|
+
|
|
861
|
+
```typescript
|
|
862
|
+
// Leave system example: memos generated from leave interactions
|
|
863
|
+
const DirectorMemo = Entity.create({
|
|
864
|
+
name: 'DirectorMemo',
|
|
865
|
+
properties: [
|
|
866
|
+
Property.create({ name: 'content', type: 'string' }),
|
|
867
|
+
Property.create({ name: 'priority', type: 'string' }),
|
|
868
|
+
Property.create({ name: 'createdAt', type: 'string' })
|
|
869
|
+
],
|
|
870
|
+
computation: Transform.create({
|
|
871
|
+
record: InteractionEventEntity,
|
|
872
|
+
attributeQuery: ['interactionName', 'user', 'payload', 'createdAt'],
|
|
873
|
+
dataDeps: {
|
|
874
|
+
users: {
|
|
875
|
+
type: 'records',
|
|
876
|
+
source: User,
|
|
877
|
+
attributeQuery: ['username', 'currentMonthLeaveCount']
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
callback: (interactionEvents, dataDeps) => {
|
|
881
|
+
// Declare data transformation relationship:
|
|
882
|
+
// Input: submitLeaveRequest interaction data + user data
|
|
883
|
+
// Output: qualifying DirectorMemo data
|
|
884
|
+
|
|
885
|
+
return interactionEvents
|
|
886
|
+
.filter(event => event.interactionName === 'submitLeaveRequest')
|
|
887
|
+
.map(event => {
|
|
888
|
+
const user = dataDeps.users.find(u => u.id === event.user.id);
|
|
889
|
+
|
|
890
|
+
// Transformation rule: when user's current month leave count >= 3, this interaction data transforms to memo data
|
|
891
|
+
if (user && user.currentMonthLeaveCount >= 3) {
|
|
892
|
+
return {
|
|
893
|
+
content: `${user.username} taking leave for the ${user.currentMonthLeaveCount}th time this month, needs attention`,
|
|
894
|
+
priority: user.currentMonthLeaveCount >= 5 ? 'urgent' : 'high',
|
|
895
|
+
createdAt: event.createdAt
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Key: return null when transformation condition not met, indicating this interaction data doesn't transform to memo
|
|
900
|
+
return null;
|
|
901
|
+
})
|
|
902
|
+
.filter(memo => memo !== null); // Filter out data that doesn't participate in transformation
|
|
903
|
+
}
|
|
904
|
+
})
|
|
905
|
+
});
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
#### One-to-Many Transform: One Interaction Data Transforms to Multiple Data Types
|
|
909
|
+
|
|
910
|
+
In real business scenarios, one interaction data can often transform into multiple different business data types, demonstrating the powerful capability of Transform's declarative transformation:
|
|
911
|
+
|
|
912
|
+
```typescript
|
|
913
|
+
// Declare how user order interaction data transforms into multiple business data types:
|
|
914
|
+
// InteractionEventEntity (createOrder) → Order, InventoryChange, PointsReward
|
|
915
|
+
|
|
916
|
+
const OrderInteraction = Interaction.create({
|
|
917
|
+
name: 'createOrder',
|
|
918
|
+
action: Action.create({ name: 'create' }),
|
|
919
|
+
payload: Payload.create({
|
|
920
|
+
items: [
|
|
921
|
+
PayloadItem.create({ name: 'orderData', base: Order }),
|
|
922
|
+
PayloadItem.create({ name: 'items', base: OrderItem, isCollection: true })
|
|
923
|
+
]
|
|
924
|
+
})
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// 1. Order records (primary transformation)
|
|
928
|
+
// Declaration: Order data is direct transformation of createOrder interaction data
|
|
929
|
+
Order.computation = Transform.create({
|
|
930
|
+
record: InteractionEventEntity,
|
|
931
|
+
callback: (interactionEvents) => {
|
|
932
|
+
return interactionEvents
|
|
933
|
+
.filter(event => event.interactionName === 'createOrder')
|
|
934
|
+
.map(event => ({
|
|
935
|
+
...event.payload.orderData,
|
|
936
|
+
createdAt: event.createdAt,
|
|
937
|
+
userId: event.user.id
|
|
938
|
+
}));
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// 2. Inventory change records (derivative transformation)
|
|
943
|
+
// Declaration: InventoryChange data is transformed from product information extracted from createOrder interaction data
|
|
944
|
+
const InventoryChange = Entity.create({
|
|
945
|
+
name: 'InventoryChange',
|
|
946
|
+
computation: Transform.create({
|
|
947
|
+
record: InteractionEventEntity,
|
|
948
|
+
callback: (interactionEvents) => {
|
|
949
|
+
const changes = [];
|
|
950
|
+
|
|
951
|
+
interactionEvents
|
|
952
|
+
.filter(event => event.interactionName === 'createOrder')
|
|
953
|
+
.forEach(event => {
|
|
954
|
+
// Extract order items from interaction data, transform to inventory change data
|
|
955
|
+
event.payload.items.forEach(item => {
|
|
956
|
+
changes.push({
|
|
957
|
+
productId: item.productId,
|
|
958
|
+
changeAmount: -item.quantity,
|
|
959
|
+
reason: 'order_created',
|
|
960
|
+
orderId: event.id,
|
|
961
|
+
createdAt: event.createdAt
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
return changes;
|
|
967
|
+
}
|
|
968
|
+
})
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
// 3. Points reward (conditional transformation)
|
|
972
|
+
// Declaration: PointsReward data is transformation result of createOrder interaction data meeting amount condition
|
|
973
|
+
const PointsReward = Entity.create({
|
|
974
|
+
name: 'PointsReward',
|
|
975
|
+
computation: Transform.create({
|
|
976
|
+
record: InteractionEventEntity,
|
|
977
|
+
callback: (interactionEvents) => {
|
|
978
|
+
return interactionEvents
|
|
979
|
+
.filter(event => event.interactionName === 'createOrder')
|
|
980
|
+
.map(event => {
|
|
981
|
+
const orderTotal = event.payload.orderData.totalAmount;
|
|
982
|
+
|
|
983
|
+
// Transformation condition: only order interaction data with amount > 100 transforms to points reward
|
|
984
|
+
if (orderTotal > 100) {
|
|
985
|
+
return {
|
|
986
|
+
userId: event.user.id,
|
|
987
|
+
points: Math.floor(orderTotal / 10),
|
|
988
|
+
reason: 'order_reward',
|
|
989
|
+
orderId: event.id,
|
|
990
|
+
createdAt: event.createdAt
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return null; // Small order interaction data doesn't transform to points
|
|
995
|
+
})
|
|
996
|
+
.filter(reward => reward !== null);
|
|
997
|
+
}
|
|
998
|
+
})
|
|
999
|
+
});
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
#### Interaction-Driven vs State-Driven Choice
|
|
1003
|
+
|
|
1004
|
+
Choose transformation from interaction data or state data based on business semantics:
|
|
1005
|
+
|
|
1006
|
+
```typescript
|
|
1007
|
+
// Interaction-driven: suitable for "each X interaction may transform to Y data"
|
|
1008
|
+
// Emphasizes: specific interaction behavior itself produces specific business data
|
|
1009
|
+
const LoginBonusPoints = Entity.create({
|
|
1010
|
+
name: 'LoginBonusPoints',
|
|
1011
|
+
computation: Transform.create({
|
|
1012
|
+
record: InteractionEventEntity, // Transform from interaction data
|
|
1013
|
+
callback: (interactionEvents) => {
|
|
1014
|
+
return interactionEvents
|
|
1015
|
+
.filter(event => event.interactionName === 'userLogin')
|
|
1016
|
+
.map(event => {
|
|
1017
|
+
// Each login interaction may transform to login reward data
|
|
1018
|
+
return isFirstLoginToday(event) ? createLoginBonus(event) : null;
|
|
1019
|
+
})
|
|
1020
|
+
.filter(bonus => bonus !== null);
|
|
1021
|
+
}
|
|
1022
|
+
})
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// State-driven: suitable for "when entity state is X, Y data should exist"
|
|
1026
|
+
// Emphasizes: derive data based on entity's current state
|
|
1027
|
+
const VIPStatus = Entity.create({
|
|
1028
|
+
name: 'VIPStatus',
|
|
1029
|
+
computation: Transform.create({
|
|
1030
|
+
record: User, // Transform from user state data
|
|
1031
|
+
callback: (users) => {
|
|
1032
|
+
return users
|
|
1033
|
+
.filter(user => user.totalSpent > 10000) // State transformation condition
|
|
1034
|
+
.map(user => ({
|
|
1035
|
+
userId: user.id,
|
|
1036
|
+
level: calculateVIPLevel(user.totalSpent),
|
|
1037
|
+
activatedAt: new Date().toISOString()
|
|
1038
|
+
}));
|
|
1039
|
+
}
|
|
1040
|
+
})
|
|
1041
|
+
});
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
#### Best Practices
|
|
1045
|
+
|
|
1046
|
+
1. **Prioritize interaction-driven**: When business data is directly related to user behavior
|
|
1047
|
+
2. **Clear data lineage**: Every Transform-generated data can trace back to specific source data
|
|
1048
|
+
3. **Good use of null returns**: Make conditional transformation logic concise and clear
|
|
1049
|
+
4. **One data source, multiple Transforms**: Don't handle all transformation logic in one Transform
|
|
1050
|
+
|
|
1051
|
+
```typescript
|
|
1052
|
+
// ✅ Good practice: separation of transformation responsibilities
|
|
1053
|
+
Order.computation = Transform.create({ /* Only responsible for transforming to order data */ });
|
|
1054
|
+
InventoryChange.computation = Transform.create({ /* Only responsible for transforming to inventory change data */ });
|
|
1055
|
+
PointsReward.computation = Transform.create({ /* Only responsible for transforming to points reward data */ });
|
|
1056
|
+
|
|
1057
|
+
// ❌ Bad practice: mixed transformation responsibilities
|
|
1058
|
+
Order.computation = Transform.create({
|
|
1059
|
+
callback: (interactionEvents) => {
|
|
1060
|
+
// Here both transforming to orders, inventory changes, and points...
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
**Core Understanding**: Transform's essence is **declarative data transformation relationships**, not traditional event callbacks. Each user interaction data can transform into multiple business data types, this **data→data** transformation mapping makes business logic clear, maintainable, and automatically responsive.
|
|
1066
|
+
|
|
1067
|
+
**Key Difference**:
|
|
1068
|
+
- **Traditional event-driven**: When event occurs → Execute callback function → Manually create data
|
|
1069
|
+
- **interaqt Transform**: Declare data transformation relationships → Framework automatically maintains → Target data automatically updates when source data changes
|
|
1070
|
+
|
|
1071
|
+
## Using StateMachine for State Management
|
|
1072
|
+
|
|
1073
|
+
StateMachine is used for state transition-based computations, particularly suitable for workflow and state management scenarios.
|
|
1074
|
+
|
|
1075
|
+
### Basic State Machine
|
|
1076
|
+
|
|
1077
|
+
```javascript
|
|
1078
|
+
import { StateMachine } from 'interaqt';
|
|
1079
|
+
|
|
1080
|
+
const Order = Entity.create({
|
|
1081
|
+
name: 'Order',
|
|
1082
|
+
properties: [
|
|
1083
|
+
Property.create({ name: 'orderNumber', type: 'string' }),
|
|
1084
|
+
Property.create({
|
|
1085
|
+
name: 'status',
|
|
1086
|
+
type: 'string',
|
|
1087
|
+
computation: new StateMachine({
|
|
1088
|
+
states: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'],
|
|
1089
|
+
initialState: 'pending',
|
|
1090
|
+
transitions: [
|
|
1091
|
+
{ from: 'pending', to: 'paid', condition: 'payment_received' },
|
|
1092
|
+
{ from: 'paid', to: 'shipped', condition: 'order_shipped' },
|
|
1093
|
+
{ from: 'shipped', to: 'delivered', condition: 'delivery_confirmed' },
|
|
1094
|
+
{ from: 'pending', to: 'cancelled', condition: 'order_cancelled' },
|
|
1095
|
+
{ from: 'paid', to: 'cancelled', condition: 'order_cancelled' }
|
|
1096
|
+
]
|
|
1097
|
+
})
|
|
1098
|
+
})
|
|
1099
|
+
]
|
|
1100
|
+
});
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
### Event-Based State Transitions
|
|
1104
|
+
|
|
1105
|
+
```javascript
|
|
1106
|
+
// Define state transition events
|
|
1107
|
+
const PaymentReceived = Interaction.create({
|
|
1108
|
+
name: 'PaymentReceived',
|
|
1109
|
+
action: Action.create({
|
|
1110
|
+
name: 'recordPayment',
|
|
1111
|
+
payload: Payload.create({
|
|
1112
|
+
items: [
|
|
1113
|
+
PayloadItem.create({ name: 'orderId', base: Order, isRef: true }),
|
|
1114
|
+
PayloadItem.create({ name: 'amount' }),
|
|
1115
|
+
PayloadItem.create({ name: 'paymentMethod' })
|
|
1116
|
+
]
|
|
1117
|
+
})
|
|
1118
|
+
})
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
// State machine listens to these events and automatically transitions states
|
|
1122
|
+
const Order = Entity.create({
|
|
1123
|
+
name: 'Order',
|
|
1124
|
+
properties: [
|
|
1125
|
+
Property.create({
|
|
1126
|
+
name: 'status',
|
|
1127
|
+
type: 'string',
|
|
1128
|
+
computation: new StateMachine({
|
|
1129
|
+
states: ['pending', 'paid', 'shipped', 'delivered'],
|
|
1130
|
+
initialState: 'pending',
|
|
1131
|
+
transitions: [
|
|
1132
|
+
{
|
|
1133
|
+
from: 'pending',
|
|
1134
|
+
to: 'paid',
|
|
1135
|
+
on: 'PaymentReceived' // Listen to interaction events
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
from: 'paid',
|
|
1139
|
+
to: 'shipped',
|
|
1140
|
+
on: 'OrderShipped'
|
|
1141
|
+
}
|
|
1142
|
+
]
|
|
1143
|
+
})
|
|
1144
|
+
})
|
|
1145
|
+
]
|
|
1146
|
+
});
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### Conditional State Transitions
|
|
1150
|
+
|
|
1151
|
+
```javascript
|
|
1152
|
+
const LeaveRequest = Entity.create({
|
|
1153
|
+
name: 'LeaveRequest',
|
|
1154
|
+
properties: [
|
|
1155
|
+
Property.create({ name: 'employeeId', type: 'string' }),
|
|
1156
|
+
Property.create({ name: 'startDate', type: 'string' }),
|
|
1157
|
+
Property.create({ name: 'endDate', type: 'string' }),
|
|
1158
|
+
Property.create({ name: 'reason', type: 'string' }),
|
|
1159
|
+
Property.create({
|
|
1160
|
+
name: 'status',
|
|
1161
|
+
type: 'string',
|
|
1162
|
+
computation: new StateMachine({
|
|
1163
|
+
states: ['draft', 'submitted', 'approved', 'rejected', 'cancelled'],
|
|
1164
|
+
initialState: 'draft',
|
|
1165
|
+
transitions: [
|
|
1166
|
+
{
|
|
1167
|
+
from: 'draft',
|
|
1168
|
+
to: 'submitted',
|
|
1169
|
+
on: 'SubmitRequest',
|
|
1170
|
+
condition: (record) => record.reason && record.startDate && record.endDate
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
from: 'submitted',
|
|
1174
|
+
to: 'approved',
|
|
1175
|
+
on: 'ApproveRequest',
|
|
1176
|
+
condition: (record, context) => context.user.role === 'manager'
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
from: 'submitted',
|
|
1180
|
+
to: 'rejected',
|
|
1181
|
+
on: 'RejectRequest',
|
|
1182
|
+
condition: (record, context) => context.user.role === 'manager'
|
|
1183
|
+
}
|
|
1184
|
+
]
|
|
1185
|
+
})
|
|
1186
|
+
})
|
|
1187
|
+
]
|
|
1188
|
+
});
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
### Dynamic Value Computation with StateNode
|
|
1192
|
+
|
|
1193
|
+
StateMachine supports dynamic value computation through the `computeValue` function in StateNode. This allows you to compute and update property values during state transitions.
|
|
1194
|
+
|
|
1195
|
+
```javascript
|
|
1196
|
+
// Example 1: Simple timestamp recording when state changes
|
|
1197
|
+
// First declare the state node
|
|
1198
|
+
const triggeredState = StateNode.create({
|
|
1199
|
+
name: 'triggered',
|
|
1200
|
+
// computeValue is called when entering this state
|
|
1201
|
+
computeValue: function(lastValue) {
|
|
1202
|
+
// Record current timestamp
|
|
1203
|
+
return Date.now();
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
const EventEntity = Entity.create({
|
|
1208
|
+
name: 'Event',
|
|
1209
|
+
properties: [
|
|
1210
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
1211
|
+
Property.create({
|
|
1212
|
+
name: 'lastTriggeredAt',
|
|
1213
|
+
type: 'number',
|
|
1214
|
+
defaultValue: () => 0,
|
|
1215
|
+
computation: StateMachine.create({
|
|
1216
|
+
states: [triggeredState],
|
|
1217
|
+
transfers: [
|
|
1218
|
+
StateTransfer.create({
|
|
1219
|
+
// Self-transition: stays in the same state but triggers computeValue
|
|
1220
|
+
current: triggeredState,
|
|
1221
|
+
next: triggeredState,
|
|
1222
|
+
trigger: TriggerEventInteraction,
|
|
1223
|
+
computeTarget: (event) => ({ id: event.payload.eventId })
|
|
1224
|
+
})
|
|
1225
|
+
],
|
|
1226
|
+
defaultState: triggeredState
|
|
1227
|
+
})
|
|
1228
|
+
})
|
|
1229
|
+
]
|
|
1230
|
+
});
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
```javascript
|
|
1234
|
+
// Example 2: Counter with dynamic increment
|
|
1235
|
+
// First declare the state nodes
|
|
1236
|
+
const idleState = StateNode.create({
|
|
1237
|
+
name: 'idle',
|
|
1238
|
+
// Keep current value when idle
|
|
1239
|
+
computeValue: function(lastValue) {
|
|
1240
|
+
return lastValue || 0;
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const incrementingState = StateNode.create({
|
|
1245
|
+
name: 'incrementing',
|
|
1246
|
+
// Increment value by 1 when entering this state
|
|
1247
|
+
computeValue: function(lastValue) {
|
|
1248
|
+
const currentValue = lastValue || 0;
|
|
1249
|
+
return currentValue + 1;
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
const CounterEntity = Entity.create({
|
|
1254
|
+
name: 'Counter',
|
|
1255
|
+
properties: [
|
|
1256
|
+
Property.create({ name: 'name', type: 'string' }),
|
|
1257
|
+
Property.create({
|
|
1258
|
+
name: 'count',
|
|
1259
|
+
type: 'number',
|
|
1260
|
+
defaultValue: () => 0,
|
|
1261
|
+
computation: StateMachine.create({
|
|
1262
|
+
states: [idleState, incrementingState],
|
|
1263
|
+
transfers: [
|
|
1264
|
+
StateTransfer.create({
|
|
1265
|
+
current: idleState,
|
|
1266
|
+
next: incrementingState,
|
|
1267
|
+
trigger: IncrementInteraction,
|
|
1268
|
+
computeTarget: (event) => ({ id: event.payload.counterId })
|
|
1269
|
+
}),
|
|
1270
|
+
StateTransfer.create({
|
|
1271
|
+
current: incrementingState,
|
|
1272
|
+
next: idleState,
|
|
1273
|
+
trigger: ResetInteraction,
|
|
1274
|
+
computeTarget: (event) => ({ id: event.payload.counterId })
|
|
1275
|
+
})
|
|
1276
|
+
],
|
|
1277
|
+
defaultState: idleState
|
|
1278
|
+
})
|
|
1279
|
+
})
|
|
1280
|
+
]
|
|
1281
|
+
});
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
```javascript
|
|
1285
|
+
// Example 3: Complex computation based on context
|
|
1286
|
+
// First declare all state nodes
|
|
1287
|
+
const newState = StateNode.create({
|
|
1288
|
+
name: 'new',
|
|
1289
|
+
computeValue: () => 10 // Base score for new tasks
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
const inProgressState = StateNode.create({
|
|
1293
|
+
name: 'inProgress',
|
|
1294
|
+
computeValue: function(lastValue) {
|
|
1295
|
+
// Add 20 points when task starts
|
|
1296
|
+
return (lastValue || 0) + 20;
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
const completedState = StateNode.create({
|
|
1301
|
+
name: 'completed',
|
|
1302
|
+
computeValue: function(lastValue) {
|
|
1303
|
+
// Double the score when completed
|
|
1304
|
+
return (lastValue || 0) * 2;
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
const cancelledState = StateNode.create({
|
|
1309
|
+
name: 'cancelled',
|
|
1310
|
+
computeValue: () => 0 // Reset score to 0 when cancelled
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
const TaskEntity = Entity.create({
|
|
1314
|
+
name: 'Task',
|
|
1315
|
+
properties: [
|
|
1316
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
1317
|
+
Property.create({ name: 'priority', type: 'number' }),
|
|
1318
|
+
Property.create({
|
|
1319
|
+
name: 'score',
|
|
1320
|
+
type: 'number',
|
|
1321
|
+
defaultValue: () => 0,
|
|
1322
|
+
computation: StateMachine.create({
|
|
1323
|
+
states: [newState, inProgressState, completedState, cancelledState],
|
|
1324
|
+
transfers: [
|
|
1325
|
+
StateTransfer.create({
|
|
1326
|
+
current: newState,
|
|
1327
|
+
next: inProgressState,
|
|
1328
|
+
trigger: StartTaskInteraction,
|
|
1329
|
+
computeTarget: (event) => ({ id: event.payload.taskId })
|
|
1330
|
+
}),
|
|
1331
|
+
StateTransfer.create({
|
|
1332
|
+
current: inProgressState,
|
|
1333
|
+
next: completedState,
|
|
1334
|
+
trigger: CompleteTaskInteraction,
|
|
1335
|
+
computeTarget: (event) => ({ id: event.payload.taskId })
|
|
1336
|
+
}),
|
|
1337
|
+
StateTransfer.create({
|
|
1338
|
+
current: inProgressState,
|
|
1339
|
+
next: cancelledState,
|
|
1340
|
+
trigger: CancelTaskInteraction,
|
|
1341
|
+
computeTarget: (event) => ({ id: event.payload.taskId })
|
|
1342
|
+
})
|
|
1343
|
+
],
|
|
1344
|
+
defaultState: newState
|
|
1345
|
+
})
|
|
1346
|
+
})
|
|
1347
|
+
]
|
|
1348
|
+
});
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
#### Key Points about computeValue
|
|
1352
|
+
|
|
1353
|
+
1. **Function Signature**: `computeValue(lastValue)` receives the last computed value as parameter
|
|
1354
|
+
2. **Return Value**: The function should return the new value for the property
|
|
1355
|
+
3. **Execution Timing**: Called when entering the state (during state transition)
|
|
1356
|
+
4. **Self-Transitions**: You can use self-transitions (same state to same state) to trigger computeValue without changing the state name
|
|
1357
|
+
5. **Initial Value**: When there's no `lastValue` (first computation), it's `undefined`, so handle this case appropriately
|
|
1358
|
+
|
|
1359
|
+
This feature is particularly useful for:
|
|
1360
|
+
- Recording timestamps of state changes
|
|
1361
|
+
- Maintaining counters and accumulators
|
|
1362
|
+
- Computing scores or metrics based on workflow progress
|
|
1363
|
+
- Any scenario where property values should change based on state transitions
|
|
1364
|
+
|
|
1365
|
+
## Using RealTime for Real-time Computations
|
|
1366
|
+
|
|
1367
|
+
RealTime computation is a core feature in the interaqt framework for handling time-sensitive data and business logic. It allows you to declare time-based computations and automatically manages computation state and recomputation timing.
|
|
1368
|
+
|
|
1369
|
+
### Understanding Real-time Computation
|
|
1370
|
+
|
|
1371
|
+
#### What is Real-time Computation
|
|
1372
|
+
|
|
1373
|
+
Real-time computation is a **time-aware reactive computation**:
|
|
1374
|
+
- **Time-driven**: Computation based on current time
|
|
1375
|
+
- **Automatic scheduling**: System automatically manages when to recompute
|
|
1376
|
+
- **State persistence**: Computation state is persistently stored
|
|
1377
|
+
- **Critical point awareness**: Can calculate critical time points for state changes
|
|
1378
|
+
|
|
1379
|
+
```typescript
|
|
1380
|
+
// Traditional time-related logic problems
|
|
1381
|
+
function checkBusinessHours() {
|
|
1382
|
+
const now = new Date();
|
|
1383
|
+
const hour = now.getHours();
|
|
1384
|
+
return hour >= 9 && hour <= 17;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// Problems:
|
|
1388
|
+
// 1. Need manual polling to check
|
|
1389
|
+
// 2. Cannot predict state change time points
|
|
1390
|
+
// 3. State is not persistent
|
|
1391
|
+
|
|
1392
|
+
// Using RealTime declarative solution
|
|
1393
|
+
const isBusinessHours = Dictionary.create({
|
|
1394
|
+
name: 'isBusinessHours',
|
|
1395
|
+
type: 'boolean',
|
|
1396
|
+
computation: RealTime.create({
|
|
1397
|
+
callback: async (now: Expression, dataDeps) => {
|
|
1398
|
+
const hour = now.divide(3600000).modulo(24); // Hour number
|
|
1399
|
+
return hour.gt(9).and(hour.lt(17));
|
|
1400
|
+
}
|
|
1401
|
+
})
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
// ✅ System automatically manages when to recompute
|
|
1405
|
+
// ✅ Automatically calculates critical change time points (9am and 5pm)
|
|
1406
|
+
// ✅ State persistently stored
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
#### RealTime vs Regular Computation
|
|
1410
|
+
|
|
1411
|
+
| Feature | RealTime Computation | Regular Reactive Computation |
|
|
1412
|
+
|---------|---------------------|------------------------------|
|
|
1413
|
+
| **Trigger Method** | Time-driven + Data-driven | Data-driven only |
|
|
1414
|
+
| **Computation Input** | Current time + Data dependencies | Data dependencies only |
|
|
1415
|
+
| **Schedule Management** | Automatic time scheduling | Data change triggered only |
|
|
1416
|
+
| **State Management** | Dual state tracking | No special state |
|
|
1417
|
+
| **Critical Prediction** | Supports critical time point calculation | Not applicable |
|
|
1418
|
+
|
|
1419
|
+
### RealTime Basic Usage
|
|
1420
|
+
|
|
1421
|
+
#### Creating Real-time Computation
|
|
1422
|
+
|
|
1423
|
+
```typescript
|
|
1424
|
+
import { RealTime, Expression, Dictionary } from 'interaqt';
|
|
1425
|
+
|
|
1426
|
+
// Basic real-time computation: current timestamp (seconds)
|
|
1427
|
+
const currentTimestamp = Dictionary.create({
|
|
1428
|
+
name: 'currentTimestamp',
|
|
1429
|
+
type: 'number',
|
|
1430
|
+
computation: RealTime.create({
|
|
1431
|
+
nextRecomputeTime: (now: number, dataDeps: any) => 1000, // Update every second
|
|
1432
|
+
callback: async (now: Expression, dataDeps: any) => {
|
|
1433
|
+
return now.divide(1000); // Convert to seconds
|
|
1434
|
+
}
|
|
1435
|
+
})
|
|
1436
|
+
});
|
|
1437
|
+
```
|
|
1438
|
+
|
|
1439
|
+
#### Expression Type Computation
|
|
1440
|
+
|
|
1441
|
+
Expression type computations return numerical results, suitable for various mathematical operations:
|
|
1442
|
+
|
|
1443
|
+
```typescript
|
|
1444
|
+
// Complex time computation
|
|
1445
|
+
const timeBasedMetric = Dictionary.create({
|
|
1446
|
+
name: 'timeBasedMetric',
|
|
1447
|
+
type: 'number',
|
|
1448
|
+
computation: RealTime.create({
|
|
1449
|
+
nextRecomputeTime: (now: number, dataDeps: any) => 5000, // Update every 5 seconds
|
|
1450
|
+
dataDeps: {
|
|
1451
|
+
config: {
|
|
1452
|
+
type: 'records',
|
|
1453
|
+
source: configEntity,
|
|
1454
|
+
attributeQuery: ['multiplier']
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
callback: async (now: Expression, dataDeps: any) => {
|
|
1458
|
+
const multiplier = dataDeps.config?.[0]?.multiplier || 1;
|
|
1459
|
+
const timeInSeconds = now.divide(1000);
|
|
1460
|
+
const timeInMinutes = now.divide(60000);
|
|
1461
|
+
|
|
1462
|
+
// Composite calculation: (time seconds * coefficient) + √(time minutes)
|
|
1463
|
+
return timeInSeconds.multiply(multiplier).add(timeInMinutes.sqrt());
|
|
1464
|
+
}
|
|
1465
|
+
})
|
|
1466
|
+
});
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
#### Inequality Type Computation
|
|
1470
|
+
|
|
1471
|
+
Inequality type computations return boolean results, and the system automatically calculates critical time points for state changes:
|
|
1472
|
+
|
|
1473
|
+
```typescript
|
|
1474
|
+
// Time threshold check
|
|
1475
|
+
const isAfterDeadline = Dictionary.create({
|
|
1476
|
+
name: 'isAfterDeadline',
|
|
1477
|
+
type: 'boolean',
|
|
1478
|
+
computation: RealTime.create({
|
|
1479
|
+
dataDeps: {
|
|
1480
|
+
project: {
|
|
1481
|
+
type: 'records',
|
|
1482
|
+
source: projectEntity,
|
|
1483
|
+
attributeQuery: ['deadline']
|
|
1484
|
+
}
|
|
1485
|
+
},
|
|
1486
|
+
callback: async (now: Expression, dataDeps: any) => {
|
|
1487
|
+
const deadline = dataDeps.project?.[0]?.deadline || Date.now() + 86400000;
|
|
1488
|
+
|
|
1489
|
+
// Check if current time exceeds deadline
|
|
1490
|
+
return now.gt(deadline);
|
|
1491
|
+
// System will automatically recompute at deadline time point
|
|
1492
|
+
}
|
|
1493
|
+
})
|
|
1494
|
+
});
|
|
1495
|
+
```
|
|
1496
|
+
|
|
1497
|
+
#### Equation Type Computation
|
|
1498
|
+
|
|
1499
|
+
Equation type is used for time equation calculations, also automatically calculates critical time points:
|
|
1500
|
+
|
|
1501
|
+
```typescript
|
|
1502
|
+
// Check if it's exact hour time
|
|
1503
|
+
const isExactHour = Dictionary.create({
|
|
1504
|
+
name: 'isExactHour',
|
|
1505
|
+
type: 'boolean',
|
|
1506
|
+
computation: RealTime.create({
|
|
1507
|
+
callback: async (now: Expression, dataDeps: any) => {
|
|
1508
|
+
const millisecondsInHour = 3600000;
|
|
1509
|
+
|
|
1510
|
+
// Check if current time is exact hour
|
|
1511
|
+
return now.modulo(millisecondsInHour).eq(0);
|
|
1512
|
+
// System will automatically calculate next exact hour time for recomputation
|
|
1513
|
+
}
|
|
1514
|
+
})
|
|
1515
|
+
});
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
### Property-level Real-time Computation
|
|
1519
|
+
|
|
1520
|
+
#### Defining Property-level Real-time Computation
|
|
1521
|
+
|
|
1522
|
+
```typescript
|
|
1523
|
+
// Define real-time computation on entity properties
|
|
1524
|
+
const userEntity = Entity.create({
|
|
1525
|
+
name: 'User',
|
|
1526
|
+
properties: [
|
|
1527
|
+
Property.create({name: 'username', type: 'string'}),
|
|
1528
|
+
Property.create({name: 'lastLoginAt', type: 'number'}),
|
|
1529
|
+
|
|
1530
|
+
// Real-time computation: whether user is recently active
|
|
1531
|
+
Property.create({
|
|
1532
|
+
name: 'isRecentlyActive',
|
|
1533
|
+
type: 'boolean',
|
|
1534
|
+
computation: RealTime.create({
|
|
1535
|
+
dataDeps: {
|
|
1536
|
+
_current: {
|
|
1537
|
+
type: 'property',
|
|
1538
|
+
attributeQuery: ['lastLoginAt']
|
|
1539
|
+
}
|
|
1540
|
+
},
|
|
1541
|
+
callback: async (now: Expression, dataDeps: any) => {
|
|
1542
|
+
const lastLogin = dataDeps._current?.lastLoginAt || 0;
|
|
1543
|
+
const oneHourAgo = now.subtract(3600000);
|
|
1544
|
+
|
|
1545
|
+
// Check if user logged in within the last hour
|
|
1546
|
+
return Expression.number(lastLogin).gt(oneHourAgo);
|
|
1547
|
+
}
|
|
1548
|
+
})
|
|
1549
|
+
}),
|
|
1550
|
+
|
|
1551
|
+
// Real-time computation: user online duration (minutes)
|
|
1552
|
+
Property.create({
|
|
1553
|
+
name: 'onlineMinutes',
|
|
1554
|
+
type: 'number',
|
|
1555
|
+
computation: RealTime.create({
|
|
1556
|
+
nextRecomputeTime: (now: number, dataDeps: any) => 60000, // Update every minute
|
|
1557
|
+
dataDeps: {
|
|
1558
|
+
_current: {
|
|
1559
|
+
type: 'property',
|
|
1560
|
+
attributeQuery: ['lastLoginAt']
|
|
1561
|
+
}
|
|
1562
|
+
},
|
|
1563
|
+
callback: async (now: Expression, dataDeps: any) => {
|
|
1564
|
+
const lastLogin = dataDeps._current?.lastLoginAt || now.evaluate({now: Date.now()});
|
|
1565
|
+
|
|
1566
|
+
// Calculate online duration (minutes)
|
|
1567
|
+
return now.subtract(lastLogin).divide(60000);
|
|
1568
|
+
}
|
|
1569
|
+
})
|
|
1570
|
+
})
|
|
1571
|
+
]
|
|
1572
|
+
});
|
|
1573
|
+
```
|
|
1574
|
+
|
|
1575
|
+
#### Property-level State Management
|
|
1576
|
+
|
|
1577
|
+
Property-level real-time computation state is stored on each record:
|
|
1578
|
+
|
|
1579
|
+
```typescript
|
|
1580
|
+
// When querying user data, state fields are automatically included
|
|
1581
|
+
const user = await system.storage.findOne('User',
|
|
1582
|
+
BoolExp.atom({key: 'id', value: ['=', userId]}),
|
|
1583
|
+
undefined,
|
|
1584
|
+
['*'] // Include all fields, including state fields
|
|
1585
|
+
);
|
|
1586
|
+
|
|
1587
|
+
// user object will contain:
|
|
1588
|
+
// {
|
|
1589
|
+
// id: 1,
|
|
1590
|
+
// username: 'john',
|
|
1591
|
+
// lastLoginAt: 1234567890000,
|
|
1592
|
+
// isRecentlyActive: true,
|
|
1593
|
+
// onlineMinutes: 45.2,
|
|
1594
|
+
// // State fields (automatically generated field names):
|
|
1595
|
+
// _record_boundState_User_isRecentlyActive_lastRecomputeTime: 1234567890123,
|
|
1596
|
+
// _record_boundState_User_isRecentlyActive_nextRecomputeTime: 1234567891000,
|
|
1597
|
+
// _record_boundState_User_onlineMinutes_lastRecomputeTime: 1234567890456,
|
|
1598
|
+
// _record_boundState_User_onlineMinutes_nextRecomputeTime: 1234567950456
|
|
1599
|
+
// }
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
### RealTime State Management
|
|
1603
|
+
|
|
1604
|
+
#### State Fields
|
|
1605
|
+
|
|
1606
|
+
Each RealTime computation has two state fields:
|
|
1607
|
+
|
|
1608
|
+
- **lastRecomputeTime**: Timestamp of last computation
|
|
1609
|
+
- **nextRecomputeTime**: Timestamp of next computation
|
|
1610
|
+
|
|
1611
|
+
```typescript
|
|
1612
|
+
// State field naming rules
|
|
1613
|
+
// Global computation: _global_boundState_{computationName}_{stateName}
|
|
1614
|
+
// Property computation: _record_boundState_{entityName}_{propertyName}_{stateName}
|
|
1615
|
+
|
|
1616
|
+
// Example state field names:
|
|
1617
|
+
// _global_boundState_currentTimestamp_lastRecomputeTime
|
|
1618
|
+
// _global_boundState_currentTimestamp_nextRecomputeTime
|
|
1619
|
+
// _record_boundState_User_isRecentlyActive_lastRecomputeTime
|
|
1620
|
+
// _record_boundState_User_isRecentlyActive_nextRecomputeTime
|
|
1621
|
+
```
|
|
1622
|
+
|
|
1623
|
+
#### State Computation Logic
|
|
1624
|
+
|
|
1625
|
+
State calculation depends on return value type:
|
|
1626
|
+
|
|
1627
|
+
```typescript
|
|
1628
|
+
// Expression type: nextRecomputeTime = lastRecomputeTime + nextRecomputeTime function return value
|
|
1629
|
+
RealTime.create({
|
|
1630
|
+
nextRecomputeTime: (now: number, dataDeps: any) => 1000, // Recompute in 1 second
|
|
1631
|
+
callback: async (now: Expression, dataDeps: any) => {
|
|
1632
|
+
return now.divide(1000); // Return Expression
|
|
1633
|
+
}
|
|
1634
|
+
// nextRecomputeTime will be lastRecomputeTime + 1000
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
// Inequality/Equation type: nextRecomputeTime = solve() result
|
|
1638
|
+
RealTime.create({
|
|
1639
|
+
callback: async (now: Expression, dataDeps: any) => {
|
|
1640
|
+
const deadline = 1640995200000;
|
|
1641
|
+
return now.gt(deadline); // Return Inequality
|
|
1642
|
+
}
|
|
1643
|
+
// nextRecomputeTime will be 1640995200000 (critical time point)
|
|
1644
|
+
});
|
|
1645
|
+
```
|
|
1646
|
+
|
|
1647
|
+
### RealTime Practical Application Scenarios
|
|
1648
|
+
|
|
1649
|
+
#### Business Hours Check
|
|
1650
|
+
|
|
1651
|
+
```typescript
|
|
1652
|
+
// Working hours check
|
|
1653
|
+
const isWorkingHours = Dictionary.create({
|
|
1654
|
+
name: 'isWorkingHours',
|
|
1655
|
+
type: 'boolean',
|
|
1656
|
+
computation: RealTime.create({
|
|
1657
|
+
dataDeps: {
|
|
1658
|
+
schedule: {
|
|
1659
|
+
type: 'records',
|
|
1660
|
+
source: scheduleEntity,
|
|
1661
|
+
attributeQuery: ['startTime', 'endTime', 'timezone']
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
callback: async (now: Expression, dataDeps: any) => {
|
|
1665
|
+
const schedule = dataDeps.schedule?.[0] || {};
|
|
1666
|
+
const startTime = schedule.startTime || 9; // 9 AM
|
|
1667
|
+
const endTime = schedule.endTime || 17; // 5 PM
|
|
1668
|
+
|
|
1669
|
+
// Calculate current hour (considering timezone)
|
|
1670
|
+
const currentHour = now.divide(3600000).modulo(24);
|
|
1671
|
+
|
|
1672
|
+
return currentHour.gt(startTime).and(currentHour.lt(endTime));
|
|
1673
|
+
}
|
|
1674
|
+
})
|
|
1675
|
+
});
|
|
1676
|
+
```
|
|
1677
|
+
|
|
1678
|
+
#### User Session Management
|
|
1679
|
+
|
|
1680
|
+
```typescript
|
|
1681
|
+
// User session expiration check
|
|
1682
|
+
const userEntity = Entity.create({
|
|
1683
|
+
name: 'User',
|
|
1684
|
+
properties: [
|
|
1685
|
+
Property.create({name: 'username', type: 'string'}),
|
|
1686
|
+
Property.create({name: 'lastActivityAt', type: 'number'}),
|
|
1687
|
+
|
|
1688
|
+
Property.create({
|
|
1689
|
+
name: 'sessionExpired',
|
|
1690
|
+
type: 'boolean',
|
|
1691
|
+
computation: RealTime.create({
|
|
1692
|
+
dataDeps: {
|
|
1693
|
+
_current: {
|
|
1694
|
+
type: 'property',
|
|
1695
|
+
attributeQuery: ['lastActivityAt']
|
|
1696
|
+
},
|
|
1697
|
+
settings: {
|
|
1698
|
+
type: 'records',
|
|
1699
|
+
source: settingsEntity,
|
|
1700
|
+
attributeQuery: ['sessionTimeout']
|
|
1701
|
+
}
|
|
1702
|
+
},
|
|
1703
|
+
callback: async (now: Expression, dataDeps: any) => {
|
|
1704
|
+
const lastActivity = dataDeps._current?.lastActivityAt || 0;
|
|
1705
|
+
const timeout = dataDeps.settings?.[0]?.sessionTimeout || 3600000; // 1 hour
|
|
1706
|
+
const expireTime = lastActivity + timeout;
|
|
1707
|
+
|
|
1708
|
+
return now.gt(expireTime);
|
|
1709
|
+
}
|
|
1710
|
+
})
|
|
1711
|
+
})
|
|
1712
|
+
]
|
|
1713
|
+
});
|
|
1714
|
+
```
|
|
1715
|
+
|
|
1716
|
+
### RealTime Performance Optimization and Best Practices
|
|
1717
|
+
|
|
1718
|
+
#### Set Appropriate Recomputation Intervals
|
|
1719
|
+
|
|
1720
|
+
```typescript
|
|
1721
|
+
// ✅ Set appropriate intervals based on business needs
|
|
1722
|
+
const highFrequency = RealTime.create({
|
|
1723
|
+
nextRecomputeTime: (now, dataDeps) => 1000, // High frequency: every second
|
|
1724
|
+
callback: async (now, dataDeps) => {
|
|
1725
|
+
// For critical metrics requiring real-time updates
|
|
1726
|
+
return now.divide(1000);
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
const mediumFrequency = RealTime.create({
|
|
1731
|
+
nextRecomputeTime: (now, dataDeps) => 60000, // Medium frequency: every minute
|
|
1732
|
+
callback: async (now, dataDeps) => {
|
|
1733
|
+
// For general business status checks
|
|
1734
|
+
return now.modulo(3600000).eq(0);
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
const lowFrequency = RealTime.create({
|
|
1739
|
+
nextRecomputeTime: (now, dataDeps) => 3600000, // Low frequency: every hour
|
|
1740
|
+
callback: async (now, dataDeps) => {
|
|
1741
|
+
// For report statistics and other non-critical data
|
|
1742
|
+
return now.divide(86400000);
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1746
|
+
// ❌ Avoid overly frequent updates
|
|
1747
|
+
const tooFrequent = RealTime.create({
|
|
1748
|
+
nextRecomputeTime: (now, dataDeps) => 100, // Every 100ms update, may affect performance
|
|
1749
|
+
callback: async (now, dataDeps) => now.divide(1000)
|
|
1750
|
+
});
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
#### Proper Use of Inequality/Equation Types
|
|
1754
|
+
|
|
1755
|
+
```typescript
|
|
1756
|
+
// ✅ Use Inequality to let system automatically calculate optimal recomputation time
|
|
1757
|
+
const smartScheduling = RealTime.create({
|
|
1758
|
+
// No need for nextRecomputeTime function
|
|
1759
|
+
callback: async (now, dataDeps) => {
|
|
1760
|
+
const deadline = 1640995200000;
|
|
1761
|
+
return now.gt(deadline); // System will automatically recompute at deadline time point
|
|
1762
|
+
}
|
|
1763
|
+
});
|
|
1764
|
+
|
|
1765
|
+
// ❌ Unnecessary manual scheduling
|
|
1766
|
+
const manualScheduling = RealTime.create({
|
|
1767
|
+
nextRecomputeTime: (now, dataDeps) => {
|
|
1768
|
+
const deadline = 1640995200000;
|
|
1769
|
+
return deadline - now; // Manual interval calculation, not as good as letting system handle automatically
|
|
1770
|
+
},
|
|
1771
|
+
callback: async (now, dataDeps) => {
|
|
1772
|
+
const deadline = 1640995200000;
|
|
1773
|
+
return now.evaluate({now: Date.now()}) > deadline;
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
```
|
|
1777
|
+
|
|
1778
|
+
## Combining Multiple Computation Types
|
|
1779
|
+
|
|
1780
|
+
In real applications, you typically need to combine multiple computation types:
|
|
1781
|
+
|
|
1782
|
+
```javascript
|
|
1783
|
+
const Post = Entity.create({
|
|
1784
|
+
name: 'Post',
|
|
1785
|
+
properties: [
|
|
1786
|
+
Property.create({ name: 'title', type: 'string' }),
|
|
1787
|
+
Property.create({ name: 'content', type: 'string' }),
|
|
1788
|
+
Property.create({ name: 'createdAt', type: 'string' }),
|
|
1789
|
+
|
|
1790
|
+
// Count: Count likes
|
|
1791
|
+
Property.create({
|
|
1792
|
+
name: 'likeCount',
|
|
1793
|
+
type: 'number',
|
|
1794
|
+
defaultValue: () => 0,
|
|
1795
|
+
computation: Count.create({
|
|
1796
|
+
record: PostLikes
|
|
1797
|
+
})
|
|
1798
|
+
}),
|
|
1799
|
+
|
|
1800
|
+
// Count: Count comments
|
|
1801
|
+
Property.create({
|
|
1802
|
+
name: 'commentCount',
|
|
1803
|
+
type: 'number',
|
|
1804
|
+
defaultValue: () => 0,
|
|
1805
|
+
computation: Count.create({
|
|
1806
|
+
record: PostComments
|
|
1807
|
+
})
|
|
1808
|
+
}),
|
|
1809
|
+
|
|
1810
|
+
// WeightedSummation: Calculate total engagement score
|
|
1811
|
+
Property.create({
|
|
1812
|
+
name: 'engagementScore',
|
|
1813
|
+
type: 'number',
|
|
1814
|
+
defaultValue: () => 0,
|
|
1815
|
+
computation: WeightedSummation.create({
|
|
1816
|
+
record: PostInteractions,
|
|
1817
|
+
callback: (relation) => {
|
|
1818
|
+
const interaction = relation.target;
|
|
1819
|
+
switch (interaction.type) {
|
|
1820
|
+
case 'like': return { weight: 1, value: 1 };
|
|
1821
|
+
case 'comment': return { weight: 1, value: 3 };
|
|
1822
|
+
case 'share': return { weight: 1, value: 5 };
|
|
1823
|
+
default: return { weight: 0, value: 0 };
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
})
|
|
1827
|
+
}),
|
|
1828
|
+
|
|
1829
|
+
Property.create({
|
|
1830
|
+
name: 'summary',
|
|
1831
|
+
type: 'string',
|
|
1832
|
+
defaultValue: () => '',
|
|
1833
|
+
computed: function(post) {
|
|
1834
|
+
const content = post.content || '';
|
|
1835
|
+
return content.length > 100
|
|
1836
|
+
? content.substring(0, 100) + '...'
|
|
1837
|
+
: content;
|
|
1838
|
+
}
|
|
1839
|
+
}),
|
|
1840
|
+
|
|
1841
|
+
// Every: Check if all comments are moderated
|
|
1842
|
+
Property.create({
|
|
1843
|
+
name: 'allCommentsModerated',
|
|
1844
|
+
type: 'boolean',
|
|
1845
|
+
defaultValue: () => false,
|
|
1846
|
+
computation: Every.create({
|
|
1847
|
+
record: PostComments,
|
|
1848
|
+
callback: (relation) => relation.target.status === 'approved'
|
|
1849
|
+
})
|
|
1850
|
+
})
|
|
1851
|
+
]
|
|
1852
|
+
});
|
|
1853
|
+
```
|
|
1854
|
+
|
|
1855
|
+
## Performance Optimization and Best Practices
|
|
1856
|
+
|
|
1857
|
+
### 1. Choose Appropriate Computation Types
|
|
1858
|
+
|
|
1859
|
+
```javascript
|
|
1860
|
+
// ✅ For simple counting, use Count
|
|
1861
|
+
Property.create({
|
|
1862
|
+
name: 'followerCount',
|
|
1863
|
+
type: 'number',
|
|
1864
|
+
defaultValue: () => 0,
|
|
1865
|
+
computation: Count.create({
|
|
1866
|
+
record: Follow
|
|
1867
|
+
})
|
|
1868
|
+
});
|
|
1869
|
+
|
|
1870
|
+
// ❌ Avoid using Transform for simple counting
|
|
1871
|
+
Property.create({
|
|
1872
|
+
name: 'followerCount',
|
|
1873
|
+
type: 'number',
|
|
1874
|
+
defaultValue: () => 0,
|
|
1875
|
+
computation: Transform.create({
|
|
1876
|
+
record: Follow,
|
|
1877
|
+
callback: (followers) => followers.length // Inefficient
|
|
1878
|
+
})
|
|
1879
|
+
});
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
### 2. Use Conditional Filtering Appropriately
|
|
1883
|
+
|
|
1884
|
+
```javascript
|
|
1885
|
+
// ✅ Use conditional filtering in computations
|
|
1886
|
+
Property.create({
|
|
1887
|
+
name: 'activeUserCount',
|
|
1888
|
+
type: 'number',
|
|
1889
|
+
defaultValue: () => 0,
|
|
1890
|
+
computation: Count.create({
|
|
1891
|
+
record: User,
|
|
1892
|
+
callback: (user) => user.status === 'active'
|
|
1893
|
+
})
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
// ❌ Avoid filtering in Transform
|
|
1897
|
+
Property.create({
|
|
1898
|
+
name: 'activeUserCount',
|
|
1899
|
+
type: 'number',
|
|
1900
|
+
defaultValue: () => 0,
|
|
1901
|
+
computation: Transform.create({
|
|
1902
|
+
record: User,
|
|
1903
|
+
callback: (users) => users.filter(u => u.status === 'active').length // Memory filtering
|
|
1904
|
+
})
|
|
1905
|
+
});
|
|
1906
|
+
```
|
|
1907
|
+
|
|
1908
|
+
### 3. Avoid Circular Dependencies
|
|
1909
|
+
|
|
1910
|
+
```javascript
|
|
1911
|
+
// ❌ Avoid circular dependencies
|
|
1912
|
+
const User = Entity.create({
|
|
1913
|
+
properties: [
|
|
1914
|
+
Property.create({
|
|
1915
|
+
name: 'score',
|
|
1916
|
+
type: 'number',
|
|
1917
|
+
defaultValue: () => 0,
|
|
1918
|
+
computation: Transform.create({
|
|
1919
|
+
record: UserPosts,
|
|
1920
|
+
callback: (posts) => posts.reduce((sum, p) => sum + p.userScore, 0)
|
|
1921
|
+
})
|
|
1922
|
+
})
|
|
1923
|
+
]
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
const Post = Entity.create({
|
|
1927
|
+
properties: [
|
|
1928
|
+
Property.create({
|
|
1929
|
+
name: 'userScore',
|
|
1930
|
+
type: 'number',
|
|
1931
|
+
defaultValue: () => 0,
|
|
1932
|
+
computation: Transform.create({
|
|
1933
|
+
record: Post,
|
|
1934
|
+
callback: (record) => record.baseScore * 0.1 // Avoid circular dependency
|
|
1935
|
+
})
|
|
1936
|
+
})
|
|
1937
|
+
]
|
|
1938
|
+
});
|
|
1939
|
+
```
|
|
1940
|
+
|
|
1941
|
+
### 4. Use Indexes to Optimize Queries
|
|
1942
|
+
|
|
1943
|
+
```javascript
|
|
1944
|
+
// Add indexes for frequently used fields in computations
|
|
1945
|
+
const Post = Entity.create({
|
|
1946
|
+
name: 'Post',
|
|
1947
|
+
properties: [
|
|
1948
|
+
Property.create({
|
|
1949
|
+
name: 'status',
|
|
1950
|
+
type: 'string',
|
|
1951
|
+
index: true // Add index
|
|
1952
|
+
}),
|
|
1953
|
+
Property.create({
|
|
1954
|
+
name: 'publishedPostCount',
|
|
1955
|
+
type: 'number',
|
|
1956
|
+
defaultValue: () => 0,
|
|
1957
|
+
computation: Count.create({
|
|
1958
|
+
record: UserPosts
|
|
1959
|
+
})
|
|
1960
|
+
})
|
|
1961
|
+
]
|
|
1962
|
+
});
|
|
1963
|
+
```
|
|
1964
|
+
|
|
1965
|
+
## Debugging and Monitoring
|
|
1966
|
+
|
|
1967
|
+
### 1. Enable Computation Logging
|
|
1968
|
+
|
|
1969
|
+
```javascript
|
|
1970
|
+
// Enable detailed logging in development environment
|
|
1971
|
+
const system = new System({
|
|
1972
|
+
logging: {
|
|
1973
|
+
computation: true,
|
|
1974
|
+
level: 'debug'
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
```
|
|
1978
|
+
|
|
1979
|
+
### 2. Monitor Computation Performance
|
|
1980
|
+
|
|
1981
|
+
```javascript
|
|
1982
|
+
// Monitor computation execution time
|
|
1983
|
+
const Post = Entity.create({
|
|
1984
|
+
properties: [
|
|
1985
|
+
Property.create({
|
|
1986
|
+
name: 'complexScore',
|
|
1987
|
+
type: 'number',
|
|
1988
|
+
computation: new Transform(
|
|
1989
|
+
Post,
|
|
1990
|
+
null,
|
|
1991
|
+
(record) => {
|
|
1992
|
+
console.time(`complexScore-${record.id}`);
|
|
1993
|
+
const result = /* complex computation */;
|
|
1994
|
+
console.timeEnd(`complexScore-${record.id}`);
|
|
1995
|
+
return result;
|
|
1996
|
+
}
|
|
1997
|
+
)
|
|
1998
|
+
})
|
|
1999
|
+
]
|
|
2000
|
+
});
|
|
2001
|
+
```
|
|
2002
|
+
|
|
2003
|
+
Reactive computation is the core advantage of interaqt. By appropriately using various computation types, you can greatly simplify business logic implementation while ensuring data consistency and system performance.
|
|
2004
|
+
|
|
2005
|
+
## Best Practices for Module Organization and Forward References
|
|
2006
|
+
|
|
2007
|
+
### The Forward Reference Problem
|
|
2008
|
+
|
|
2009
|
+
When defining computed properties that reference relations not yet defined in the same file, you might encounter forward reference issues:
|
|
2010
|
+
|
|
2011
|
+
```javascript
|
|
2012
|
+
// ❌ WRONG: Using function form to "solve" forward reference
|
|
2013
|
+
const Version = Entity.create({
|
|
2014
|
+
name: 'Version',
|
|
2015
|
+
properties: [
|
|
2016
|
+
Property.create({
|
|
2017
|
+
name: 'styleCount',
|
|
2018
|
+
type: 'number',
|
|
2019
|
+
computation: Count.create({
|
|
2020
|
+
record: () => StyleVersionRelation // ❌ Function form is NOT the solution
|
|
2021
|
+
})
|
|
2022
|
+
})
|
|
2023
|
+
]
|
|
2024
|
+
});
|
|
2025
|
+
|
|
2026
|
+
// StyleVersionRelation defined later or imported at bottom
|
|
2027
|
+
import { StyleVersionRelation } from '../relations/StyleVersionRelation'
|
|
2028
|
+
```
|
|
2029
|
+
|
|
2030
|
+
### Correct Solutions
|
|
2031
|
+
|
|
2032
|
+
#### Solution 1: Organize File Structure Properly
|
|
2033
|
+
|
|
2034
|
+
Structure your files to avoid forward references:
|
|
2035
|
+
|
|
2036
|
+
```javascript
|
|
2037
|
+
// relations/StyleVersionRelation.ts
|
|
2038
|
+
import { Relation } from 'interaqt'
|
|
2039
|
+
import { Style } from '../entities/Style'
|
|
2040
|
+
import { Version } from '../entities/Version'
|
|
2041
|
+
|
|
2042
|
+
export const StyleVersionRelation = Relation.create({
|
|
2043
|
+
source: Style,
|
|
2044
|
+
target: Version,
|
|
2045
|
+
type: 'n:n'
|
|
2046
|
+
})
|
|
2047
|
+
|
|
2048
|
+
// entities/Version.ts
|
|
2049
|
+
import { Entity, Property, Count } from 'interaqt'
|
|
2050
|
+
import { StyleVersionRelation } from '../relations/StyleVersionRelation'
|
|
2051
|
+
|
|
2052
|
+
export const Version = Entity.create({
|
|
2053
|
+
name: 'Version',
|
|
2054
|
+
properties: [
|
|
2055
|
+
Property.create({
|
|
2056
|
+
name: 'styleCount',
|
|
2057
|
+
type: 'number',
|
|
2058
|
+
computation: Count.create({
|
|
2059
|
+
record: StyleVersionRelation // ✅ Direct reference, properly imported
|
|
2060
|
+
})
|
|
2061
|
+
})
|
|
2062
|
+
]
|
|
2063
|
+
})
|
|
2064
|
+
```
|
|
2065
|
+
|
|
2066
|
+
#### Solution 2: Define Basic Structure First, Add Computed Properties Later
|
|
2067
|
+
|
|
2068
|
+
If you have circular dependencies between entities and relations:
|
|
2069
|
+
|
|
2070
|
+
```javascript
|
|
2071
|
+
// entities/Version.ts - Step 1: Define basic entity
|
|
2072
|
+
export const Version = Entity.create({
|
|
2073
|
+
name: 'Version',
|
|
2074
|
+
properties: [
|
|
2075
|
+
Property.create({ name: 'versionNumber', type: 'number' }),
|
|
2076
|
+
Property.create({ name: 'name', type: 'string' })
|
|
2077
|
+
// Don't add computed properties that depend on relations yet
|
|
2078
|
+
]
|
|
2079
|
+
})
|
|
2080
|
+
|
|
2081
|
+
// relations/StyleVersionRelation.ts - Step 2: Define relations
|
|
2082
|
+
import { Version } from '../entities/Version'
|
|
2083
|
+
import { Style } from '../entities/Style'
|
|
2084
|
+
|
|
2085
|
+
export const StyleVersionRelation = Relation.create({
|
|
2086
|
+
source: Style,
|
|
2087
|
+
target: Version,
|
|
2088
|
+
type: 'n:n'
|
|
2089
|
+
})
|
|
2090
|
+
|
|
2091
|
+
// setup/computedProperties.ts - Step 3: Add computed properties
|
|
2092
|
+
import { Property, Count } from 'interaqt'
|
|
2093
|
+
import { Version } from '../entities/Version'
|
|
2094
|
+
import { StyleVersionRelation } from '../relations/StyleVersionRelation'
|
|
2095
|
+
|
|
2096
|
+
// Add computed properties after all entities and relations are defined
|
|
2097
|
+
Version.properties.push(
|
|
2098
|
+
Property.create({
|
|
2099
|
+
name: 'styleCount',
|
|
2100
|
+
type: 'number',
|
|
2101
|
+
computation: Count.create({
|
|
2102
|
+
record: StyleVersionRelation // ✅ Now safely reference the relation
|
|
2103
|
+
})
|
|
2104
|
+
})
|
|
2105
|
+
)
|
|
2106
|
+
```
|
|
2107
|
+
|
|
2108
|
+
### Key Principles
|
|
2109
|
+
|
|
2110
|
+
1. **Never use function form for record parameter**: The `record` parameter in Count, Transform, etc. should always be a direct reference to an Entity or Relation, never a function.
|
|
2111
|
+
|
|
2112
|
+
2. **Avoid circular references**: Never reference the entity being defined in its own Transform computation.
|
|
2113
|
+
|
|
2114
|
+
3. **Proper import order**: Ensure dependencies are imported before they're used.
|
|
2115
|
+
|
|
2116
|
+
4. **File organization matters**: Structure your modules to minimize forward references:
|
|
2117
|
+
```
|
|
2118
|
+
entities/
|
|
2119
|
+
├── base/ # Basic entities without computed properties
|
|
2120
|
+
├── index.ts # Export all entities
|
|
2121
|
+
relations/
|
|
2122
|
+
├── index.ts # Export all relations
|
|
2123
|
+
computed/
|
|
2124
|
+
└── setup.ts # Add computed properties that depend on relations
|
|
2125
|
+
```
|
|
2126
|
+
|
|
2127
|
+
5. **Use getValue or computed for same-entity computations**: For computed properties that only depend on the same entity's data, use `getValue` or `computed` instead of Transform:
|
|
2128
|
+
```javascript
|
|
2129
|
+
Property.create({
|
|
2130
|
+
name: 'displayName',
|
|
2131
|
+
type: 'string',
|
|
2132
|
+
getValue: (record) => `${record.firstName} ${record.lastName}` // ✅ Simple, same-entity computation
|
|
2133
|
+
})
|
|
2134
|
+
// or
|
|
2135
|
+
Property.create({
|
|
2136
|
+
name: 'displayName',
|
|
2137
|
+
type: 'string',
|
|
2138
|
+
computed: function(record) {
|
|
2139
|
+
return `${record.firstName} ${record.lastName}`; // ✅ Also correct
|
|
2140
|
+
}
|
|
2141
|
+
})
|
|
2142
|
+
```
|
|
2143
|
+
|
|
2144
|
+
### Common Mistakes to Avoid
|
|
2145
|
+
|
|
2146
|
+
```javascript
|
|
2147
|
+
// ❌ DON'T: Use arrow functions for record parameter
|
|
2148
|
+
computation: Count.create({
|
|
2149
|
+
record: () => SomeRelation // This is NOT how to handle forward references
|
|
2150
|
+
})
|
|
2151
|
+
|
|
2152
|
+
// ❌ DON'T: Use Transform for property computation
|
|
2153
|
+
const Version = Entity.create({
|
|
2154
|
+
name: 'Version',
|
|
2155
|
+
properties: [
|
|
2156
|
+
Property.create({
|
|
2157
|
+
name: 'nextVersionNumber',
|
|
2158
|
+
computation: Transform.create({
|
|
2159
|
+
record: Version // Wrong! Transform is for collection-to-collection transformation, not property computation
|
|
2160
|
+
})
|
|
2161
|
+
})
|
|
2162
|
+
]
|
|
2163
|
+
})
|
|
2164
|
+
|
|
2165
|
+
// ❌ DON'T: Use Transform for property-level calculations
|
|
2166
|
+
Property.create({
|
|
2167
|
+
name: 'formattedPrice',
|
|
2168
|
+
computation: Transform.create({
|
|
2169
|
+
record: Product, // Wrong! Transform cannot be used for property computation
|
|
2170
|
+
callback: (product) => `$${product.price}`
|
|
2171
|
+
})
|
|
2172
|
+
})
|
|
2173
|
+
|
|
2174
|
+
// ✅ DO: Use getValue or computed for property-level computations
|
|
2175
|
+
Property.create({
|
|
2176
|
+
name: 'formattedPrice',
|
|
2177
|
+
type: 'string',
|
|
2178
|
+
getValue: (record) => `$${record.price}` // Correct! getValue is for same-entity property computation
|
|
2179
|
+
})
|
|
2180
|
+
|
|
2181
|
+
// ✅ DO: Use proper imports and direct references
|
|
2182
|
+
import { StyleVersionRelation } from '../relations/StyleVersionRelation'
|
|
2183
|
+
|
|
2184
|
+
computation: Count.create({
|
|
2185
|
+
record: StyleVersionRelation // Direct reference
|
|
2186
|
+
})
|