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.
Files changed (35) hide show
  1. package/agent/.claude/agents/code-generation-handler.md +2 -0
  2. package/agent/.claude/agents/computation-generation-handler.md +1 -0
  3. package/agent/.claude/agents/implement-design-handler.md +4 -13
  4. package/agent/.claude/agents/requirements-analysis-handler.md +46 -14
  5. package/agent/agentspace/knowledge/generator/api-reference.md +3378 -0
  6. package/agent/agentspace/knowledge/generator/basic-interaction-generation.md +377 -0
  7. package/agent/agentspace/knowledge/generator/computation-analysis.md +307 -0
  8. package/agent/agentspace/knowledge/generator/computation-implementation.md +959 -0
  9. package/agent/agentspace/knowledge/generator/data-analysis.md +463 -0
  10. package/agent/agentspace/knowledge/generator/entity-relation-generation.md +395 -0
  11. package/agent/agentspace/knowledge/generator/permission-implementation.md +460 -0
  12. package/agent/agentspace/knowledge/generator/permission-test-implementation.md +870 -0
  13. package/agent/agentspace/knowledge/generator/test-implementation.md +674 -0
  14. package/agent/agentspace/knowledge/usage/00-mindset-shift.md +322 -0
  15. package/agent/agentspace/knowledge/usage/01-core-concepts.md +131 -0
  16. package/agent/agentspace/knowledge/usage/02-define-entities-properties.md +407 -0
  17. package/agent/agentspace/knowledge/usage/03-entity-relations.md +599 -0
  18. package/agent/agentspace/knowledge/usage/04-reactive-computations.md +2186 -0
  19. package/agent/agentspace/knowledge/usage/05-interactions.md +1411 -0
  20. package/agent/agentspace/knowledge/usage/06-attributive-permissions.md +10 -0
  21. package/agent/agentspace/knowledge/usage/07-payload-parameters.md +593 -0
  22. package/agent/agentspace/knowledge/usage/08-activities.md +863 -0
  23. package/agent/agentspace/knowledge/usage/09-filtered-entities.md +784 -0
  24. package/agent/agentspace/knowledge/usage/10-async-computations.md +734 -0
  25. package/agent/agentspace/knowledge/usage/11-global-dictionaries.md +942 -0
  26. package/agent/agentspace/knowledge/usage/12-data-querying.md +1033 -0
  27. package/agent/agentspace/knowledge/usage/13-testing.md +1201 -0
  28. package/agent/agentspace/knowledge/usage/14-api-reference.md +1606 -0
  29. package/agent/agentspace/knowledge/usage/15-entity-crud-patterns.md +1122 -0
  30. package/agent/agentspace/knowledge/usage/16-frontend-page-design-guide.md +485 -0
  31. package/agent/agentspace/knowledge/usage/17-performance-optimization.md +283 -0
  32. package/agent/agentspace/knowledge/usage/18-api-exports-reference.md +176 -0
  33. package/agent/agentspace/knowledge/usage/19-common-anti-patterns.md +563 -0
  34. package/agent/agentspace/knowledge/usage/README.md +148 -0
  35. 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
+ })