interaqt 0.7.2 → 0.7.4

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.
@@ -0,0 +1,550 @@
1
+ ---
2
+ name: integration-implementation-handler
3
+ description: Guide for implementing integrations to connect reactive backend with imperative external systems
4
+ model: inherit
5
+ color: purple
6
+ ---
7
+
8
+ # Integration Implementation Guide
9
+
10
+ ## Overview
11
+
12
+ **Integrations** bridge the gap between the reactive backend framework and imperative external systems (APIs, services, databases, message queues, etc.). They allow external non-reactive systems to participate in the reactive data flow.
13
+
14
+ ## Integration Interface
15
+
16
+ Every integration must implement the `IIntegration` interface:
17
+
18
+ ```typescript
19
+ export type IIntegration = {
20
+ configure?:() => Promise<any>
21
+ setup?:(controller: Controller) => Promise<any>
22
+ createSideEffects:() => RecordMutationSideEffect[]
23
+ createAPIs?: () => APIs
24
+ }
25
+ ```
26
+
27
+ ### Constructor Arguments
28
+
29
+ ```typescript
30
+ export type IIntegrationConstructorArgs = {
31
+ entities: EntityInstance[],
32
+ relations: RelationInstance[],
33
+ activities: ActivityInstance[],
34
+ interactions: InteractionInstance[],
35
+ dict: DictionaryInstance[]
36
+ }
37
+
38
+ export type IIntegrationHandles = {
39
+ [k:string]: any // External handles like websocketServer, etc.
40
+ }
41
+
42
+ class MyIntegration implements IIntegration {
43
+ constructor(
44
+ public args: IIntegrationConstructorArgs,
45
+ public handles: IIntegrationHandles
46
+ ) {}
47
+ }
48
+ ```
49
+
50
+ ## Lifecycle Methods
51
+
52
+ ### 1. `configure()` - Schema Augmentation
53
+
54
+ **Purpose**: Modify the reactive schema before system initialization.
55
+
56
+ **Use Cases**:
57
+ - Inject new entities into the system
58
+ - Add computed properties to existing entities
59
+ - Configure state machines for reactive properties
60
+ - Inject computations into relations
61
+
62
+ **Execution Timing**: Before Controller initialization
63
+
64
+ **Example - Injecting Entity**:
65
+ ```typescript
66
+ async configure() {
67
+ // Create and inject a new entity for external events
68
+ const TaskEvent = Entity.create({
69
+ name: 'LLMPicGenAsyncTaskEvent',
70
+ properties: [
71
+ Property.create({ name: 'taskId', type: 'string' }),
72
+ Property.create({ name: 'status', type: 'string' }),
73
+ Property.create({ name: 'result', type: 'object' }),
74
+ ]
75
+ });
76
+
77
+ // Inject into entities array
78
+ this.args.entities.push(TaskEvent);
79
+ }
80
+ ```
81
+
82
+ **Example - Injecting Property Computation**:
83
+ ```typescript
84
+ async configure() {
85
+ // Find target entities
86
+ const streamEntities = Stream.instances;
87
+
88
+ for (const entity of streamEntities) {
89
+ // Find and inject computation into property
90
+ const urlProperty = entity.properties.find(p => p.name === 'url')!;
91
+
92
+ urlProperty.computation = Custom.create({
93
+ name: 'generateStreamUrl',
94
+ async getInitialValue(this: Controller, record?: any) {
95
+ // Call external API to generate URL
96
+ const timestamp = Math.floor(Date.now() / 1000) + 3600;
97
+ const authString = `/${appName}/${streamName}${key}${timestamp}`;
98
+ const sign = crypto.createHash('md5').update(authString).digest('hex');
99
+ return `rtmp://${domain}/${appName}/${streamName}?volcTime=${timestamp}&volcSecret=${sign}`;
100
+ },
101
+ incrementalCompute: async function(this: { controller: Controller, state: any }, lastValue: any, mutationEvent: any, record: any, dataDeps: any) {
102
+ // Skip recomputation if URL should remain constant
103
+ return ComputationResult.skip();
104
+ }
105
+ });
106
+ }
107
+ }
108
+ ```
109
+
110
+ **Example - Injecting State Machine**:
111
+ ```typescript
112
+ async configure() {
113
+ const taskEntities = AsyncTask.instances;
114
+
115
+ for (const taskEntity of taskEntities) {
116
+ const statusProperty = taskEntity.properties.find(p => p.name === 'status');
117
+
118
+ // Create state node
119
+ const statusState = StateNode.create({
120
+ name: 'status',
121
+ computeValue: (lastValue, mutationEvent) => {
122
+ return mutationEvent?.record?.status || lastValue || 'pending';
123
+ }
124
+ });
125
+
126
+ // Configure state machine
127
+ statusProperty.computation = StateMachine.create({
128
+ states: [statusState],
129
+ initialState: statusState,
130
+ transfers: [
131
+ StateTransfer.create({
132
+ trigger: {
133
+ recordName: 'TaskEvent', // Event entity name
134
+ type: 'create',
135
+ record: { taskType: taskEntity.name }
136
+ },
137
+ current: statusState,
138
+ next: statusState,
139
+ computeTarget: async function(this: Controller, mutationEvent: any) {
140
+ const event = mutationEvent.record;
141
+ if (event.status) {
142
+ return { id: event.taskId }; // Target record to update
143
+ }
144
+ return undefined;
145
+ }
146
+ })
147
+ ]
148
+ });
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### 2. `setup()` - Runtime Initialization
154
+
155
+ **Purpose**: Initialize external connections and register runtime handlers.
156
+
157
+ **Use Cases**:
158
+ - Connect to external services (Redis, databases, message queues)
159
+ - Register event listeners on external handles
160
+ - Set up WebSocket connection handlers
161
+ - Initialize API clients
162
+
163
+ **Execution Timing**: After Controller initialization, before server starts
164
+
165
+ **Example - External Service Connection**:
166
+ ```typescript
167
+ async setup(controller: Controller) {
168
+ const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
169
+
170
+ this.subscriber = createClient({ url: redisUrl });
171
+ this.subscriber.on('error', (err) => console.error('Redis Error:', err));
172
+ await this.subscriber.connect();
173
+
174
+ this.publisher = createClient({ url: redisUrl });
175
+ await this.publisher.connect();
176
+ }
177
+ ```
178
+
179
+ **Example - WebSocket Handler Registration**:
180
+ ```typescript
181
+ async setup(controller: Controller) {
182
+ // Register handler for user connection
183
+ this.handles.websocketServer.on('connection-setup-completed', async (ws, request) => {
184
+ const user = ws.user;
185
+ if (!user) return;
186
+
187
+ // Query user's channels
188
+ const userChannels = await controller.system.storage.find(
189
+ 'UserChannelRelation',
190
+ MatchExp.atom({ key: 'source.id', value: ['=', user.id] }),
191
+ undefined,
192
+ ['id', ['target', { attributeQuery: ['id'] }]]
193
+ );
194
+
195
+ // Subscribe to Redis channels for this user
196
+ for (let relation of userChannels) {
197
+ this.subscriber?.subscribe(relation.target.id, (message) => {
198
+ ws.send(message);
199
+ });
200
+ }
201
+ });
202
+ }
203
+ ```
204
+
205
+ ### 3. `createSideEffects()` - Reactive Synchronization
206
+
207
+ **Purpose**: Define side effects that synchronize reactive data changes to external systems.
208
+
209
+ **Use Cases**:
210
+ - Push data mutations to external APIs
211
+ - Publish messages to message queues
212
+ - Update external database records
213
+ - Trigger external workflows
214
+
215
+ **Return Value**: Array of `RecordMutationSideEffect`
216
+
217
+ **Example - Publishing to Message Queue**:
218
+ ```typescript
219
+ createSideEffects(): RecordMutationSideEffect[] {
220
+ const dispatcher = this;
221
+
222
+ return [
223
+ RecordMutationSideEffect.create({
224
+ name: 'channel_message_publish_sideeffect',
225
+ record: {
226
+ name: 'ChannelMessageRelation', // Target relation/entity
227
+ },
228
+ async content(this: Controller, event: RecordMutationEvent) {
229
+ if (event.type === 'create') {
230
+ const channelId = event.record?.source.id;
231
+ const message = event.record?.target;
232
+
233
+ if (message && channelId) {
234
+ const messageData = {
235
+ type: 'message',
236
+ channelId,
237
+ message
238
+ };
239
+
240
+ // Publish to external system
241
+ await dispatcher.publisher?.publish(
242
+ channelId,
243
+ JSON.stringify(messageData)
244
+ );
245
+ console.log('Published to Redis:', channelId);
246
+ }
247
+ }
248
+ }
249
+ })
250
+ ];
251
+ }
252
+ ```
253
+
254
+ **Example - Multiple Side Effects**:
255
+ ```typescript
256
+ createSideEffects(): RecordMutationSideEffect[] {
257
+ return [
258
+ // Handle relation creation
259
+ ...UserChannelRelation.instances.map(instance =>
260
+ RecordMutationSideEffect.create({
261
+ name: `channel_user_relation_${instance.name}_create`,
262
+ record: { name: instance.name! },
263
+ async content(this: Controller, event: RecordMutationEvent) {
264
+ if (event.type === 'create') {
265
+ // Subscribe online users to new channels
266
+ const clients = Array.from(websocketServer.clients);
267
+ const wsClient = clients.find(c => c.user.id === event.record?.source.id);
268
+ if (wsClient) {
269
+ subscriber?.subscribe(event.record?.target.id, (msg) => {
270
+ wsClient.send(msg);
271
+ });
272
+ }
273
+ }
274
+ }
275
+ })
276
+ ),
277
+
278
+ // Handle relation deletion
279
+ ...UserChannelRelation.instances.map(instance =>
280
+ RecordMutationSideEffect.create({
281
+ name: `channel_user_relation_${instance.name}_delete`,
282
+ record: { name: instance.name! },
283
+ async content(this: Controller, event: RecordMutationEvent) {
284
+ if (event.type === 'delete') {
285
+ // Unsubscribe user from channel
286
+ // ... implementation
287
+ }
288
+ }
289
+ })
290
+ )
291
+ ];
292
+ }
293
+ ```
294
+
295
+ ### 4. `createAPIs()` - Custom Endpoints
296
+
297
+ **Purpose**: Create custom API endpoints for external system interactions.
298
+
299
+ **Use Cases**:
300
+ - Query external service status
301
+ - Trigger external operations
302
+ - Proxy requests to external APIs
303
+ - Implement custom business logic that doesn't fit interactions
304
+
305
+ **Return Value**: Object mapping API names to API definitions
306
+
307
+ **Example - Status Query API**:
308
+ ```typescript
309
+ createAPIs(): APIs {
310
+ const apis: APIs = {};
311
+
312
+ apis.queryTaskStatus = createAPI(
313
+ async function(this: Controller, context, params: { taskId: string, taskType: string }) {
314
+ const { taskId, taskType } = params;
315
+
316
+ // Query internal state
317
+ const task = await this.system.storage.findOne(
318
+ taskType,
319
+ MatchExp.atom({ key: 'id', value: ['=', taskId] }),
320
+ undefined,
321
+ ['id', 'executionId', 'status', 'result']
322
+ );
323
+
324
+ if (!task) {
325
+ return { error: 'Task not found' };
326
+ }
327
+
328
+ // Query external system
329
+ const externalStatus = await queryExternalAPI(task.executionId);
330
+
331
+ // Update internal state via event creation
332
+ if (externalStatus.status !== task.status) {
333
+ await this.system.storage.create('TaskEvent', {
334
+ taskId,
335
+ eventType: 'statusUpdated',
336
+ status: externalStatus.status,
337
+ result: externalStatus.result,
338
+ taskType
339
+ });
340
+ }
341
+
342
+ return {
343
+ success: true,
344
+ taskId,
345
+ status: externalStatus.status,
346
+ result: externalStatus.result
347
+ };
348
+ },
349
+ {
350
+ params: { taskId: 'string', taskType: 'string' },
351
+ useNamedParams: true,
352
+ allowAnonymous: false
353
+ }
354
+ );
355
+
356
+ return apis;
357
+ }
358
+ ```
359
+
360
+ ## Integration Patterns
361
+
362
+ ### Pattern 1: External Service Synchronization (Redis, MQ)
363
+
364
+ **Characteristics**:
365
+ - Bidirectional data flow
366
+ - Real-time synchronization
367
+ - Connection management
368
+
369
+ **Implementation**:
370
+ - `setup()`: Establish connections, register listeners
371
+ - `createSideEffects()`: Push internal changes to external system
372
+ - External events → Create records in reactive system
373
+
374
+ **Example**: RedisChannelIntegration
375
+
376
+ ### Pattern 2: External Resource Generation (URLs, Tokens)
377
+
378
+ **Characteristics**:
379
+ - One-way data flow (internal → external)
380
+ - Lazy evaluation
381
+ - Resource lifecycle management
382
+
383
+ **Implementation**:
384
+ - `configure()`: Inject Custom computation into properties
385
+ - `getInitialValue()`: Call external API to generate resource
386
+ - `incrementalCompute()`: Usually skip (resources are immutable)
387
+
388
+ **Example**: VolcStreamIntegration
389
+
390
+ ### Pattern 3: Async Task Management
391
+
392
+ **Characteristics**:
393
+ - Async operation lifecycle
394
+ - Status polling
395
+ - Result synchronization
396
+
397
+ **Implementation**:
398
+ - `configure()`: Inject event entity, configure state machines
399
+ - `createAPIs()`: Provide status query endpoint
400
+ - Event-driven state updates via injected entity
401
+
402
+ **Example**: VolcPicGenIntegration
403
+
404
+ ## Best Practices
405
+
406
+ ### 1. Separation of Concerns
407
+
408
+ - **configure()**: Schema modifications only
409
+ - **setup()**: Connection establishment only
410
+ - **createSideEffects()**: Data synchronization only
411
+ - **createAPIs()**: Query/command operations only
412
+
413
+ ### 2. Error Handling
414
+
415
+ ```typescript
416
+ async setup(controller: Controller) {
417
+ try {
418
+ this.client = await connectToService();
419
+ this.client.on('error', (err) => {
420
+ console.error('Service error:', err);
421
+ // Implement reconnection logic
422
+ });
423
+ } catch (error) {
424
+ console.error('Failed to connect:', error);
425
+ throw error; // Fail fast if critical
426
+ }
427
+ }
428
+ ```
429
+
430
+ ### 3. Event-Driven State Updates
431
+
432
+ **❌ BAD - Direct State Mutation**:
433
+ ```typescript
434
+ // Don't directly update entity properties
435
+ await this.system.storage.update(taskType, taskId, {
436
+ status: newStatus // This bypasses reactive system
437
+ });
438
+ ```
439
+
440
+ **✅ GOOD - Event-Driven Updates**:
441
+ ```typescript
442
+ // Create event records to trigger state machines
443
+ await this.system.storage.create('TaskEvent', {
444
+ taskId,
445
+ eventType: 'statusUpdated',
446
+ status: newStatus,
447
+ taskType
448
+ });
449
+ // State machine will reactively update the target entity
450
+ ```
451
+
452
+ ### 4. Resource Cleanup
453
+
454
+ ```typescript
455
+ async cleanup() {
456
+ if (this.subscriber?.isOpen) {
457
+ await this.subscriber.disconnect();
458
+ }
459
+ if (this.publisher?.isOpen) {
460
+ await this.publisher.disconnect();
461
+ }
462
+ }
463
+ ```
464
+
465
+ ### 5. Environment Configuration
466
+
467
+ ```typescript
468
+ async setup(controller: Controller) {
469
+ const apiKey = process.env.EXTERNAL_API_KEY;
470
+ if (!apiKey) {
471
+ throw new Error('EXTERNAL_API_KEY must be set');
472
+ }
473
+ // ... use apiKey
474
+ }
475
+ ```
476
+
477
+ ### 6. Type Safety with External APIs
478
+
479
+ ```typescript
480
+ // Define external API types
481
+ export type ExternalTaskStatus = 'pending' | 'processing' | 'success' | 'failed';
482
+
483
+ // Map to internal types
484
+ function mapExternalStatus(external: string): TaskStatus {
485
+ switch (external) {
486
+ case 'in_queue': return 'pending';
487
+ case 'generating': return 'processing';
488
+ case 'done': return 'success';
489
+ default: return 'failed';
490
+ }
491
+ }
492
+ ```
493
+
494
+ ## Integration Registration
495
+
496
+ After creating an integration, register it in `integrations/index.ts`:
497
+
498
+ ```typescript
499
+ import { MyIntegration } from './myintegration';
500
+
501
+ const AggregatedIntegrationClass = createAggregatedIntegration([
502
+ RedisChannelIntegration,
503
+ VolcStreamIntegration,
504
+ VolcPicGenIntegration,
505
+ MyIntegration // Add your integration
506
+ ]);
507
+
508
+ export default AggregatedIntegrationClass;
509
+ ```
510
+
511
+ ## Testing Integrations
512
+
513
+ 1. **Unit Tests**: Test integration logic in isolation
514
+ 2. **Integration Tests**: Test with mock external services
515
+ 3. **E2E Tests**: Test with real external services in staging
516
+
517
+ ```typescript
518
+ // Mock external service for testing
519
+ class MockExternalService {
520
+ async query(id: string) {
521
+ return { status: 'success', result: { data: 'test' } };
522
+ }
523
+ }
524
+
525
+ // Use in tests
526
+ const integration = new MyIntegration(args, {
527
+ externalService: new MockExternalService()
528
+ });
529
+ ```
530
+
531
+ ## Common Pitfalls
532
+
533
+ 1. **❌ Modifying schema in setup()**: Schema must be finalized before Controller initialization
534
+ 2. **❌ Blocking operations in configure()**: Avoid heavy I/O, keep it fast
535
+ 3. **❌ Forgetting error handlers**: Always handle connection errors
536
+ 4. **❌ Direct state mutations**: Use event-driven updates instead
537
+ 5. **❌ Ignoring cleanup**: Implement proper resource cleanup
538
+ 6. **❌ Hardcoding configuration**: Use environment variables
539
+
540
+ ## Summary
541
+
542
+ Integrations enable reactive systems to communicate with external imperative systems through four key mechanisms:
543
+
544
+ 1. **configure()**: Augment reactive schema
545
+ 2. **setup()**: Initialize external connections
546
+ 3. **createSideEffects()**: Synchronize data to external systems
547
+ 4. **createAPIs()**: Expose custom endpoints
548
+
549
+ Choose the appropriate pattern based on your integration needs, and always prioritize event-driven design for state synchronization.
550
+
@@ -29,6 +29,6 @@
29
29
  6. 最终产出的 interactions-design.json 仍然使用原文档里的 interaction 数据结构,但以流程为组来组织。
30
30
 
31
31
  注意,在每一个步骤中,都要给出清晰的数据结构定义,用 json 来写。
32
- 整体用简洁的英语完成文档重写。注意原本文档中的在关键步骤 update STATUS.json 仍然按照原文档的方式写。
32
+ 整体用简洁的英语完成文档重写。注意原本文档中的在关键步骤 update docs/{module}.status.json 仍然按照原文档的方式写。
33
33
 
34
34