@majkapp/plugin-kit 3.5.5 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -0
- package/bin/promptable-cli.js +6 -0
- package/dist/generator/cli.js +90 -0
- package/dist/generator/extract-command.d.ts +10 -0
- package/dist/generator/extract-command.d.ts.map +1 -0
- package/dist/generator/extract-command.js +122 -0
- package/dist/index.d.ts +86 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -1
- package/dist/majk-interface-types.d.ts +15 -0
- package/dist/majk-interface-types.d.ts.map +1 -1
- package/dist/plugin-kit.d.ts +58 -0
- package/dist/plugin-kit.d.ts.map +1 -1
- package/dist/plugin-kit.js +50 -4
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/API.md +8 -0
- package/docs/CONTEXT.md +74 -0
- package/docs/RPC_CALLBACKS.md +518 -0
- package/package.json +8 -2
- package/dist/transports.d.ts +0 -59
- package/dist/transports.d.ts.map +0 -1
- package/dist/transports.js +0 -171
package/docs/CONTEXT.md
CHANGED
|
@@ -351,6 +351,79 @@ Subscribe to MAJK events (conversations, todos, projects, etc.).
|
|
|
351
351
|
})
|
|
352
352
|
```
|
|
353
353
|
|
|
354
|
+
## ctx.rpc
|
|
355
|
+
|
|
356
|
+
Enable inter-plugin communication with RPC services and callbacks.
|
|
357
|
+
|
|
358
|
+
### Register Service
|
|
359
|
+
|
|
360
|
+
Make your plugin's functions available to other plugins:
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
.onLoad(async (ctx) => {
|
|
364
|
+
// Register a service that other plugins can call
|
|
365
|
+
await ctx.rpc.registerService('fileProcessor', {
|
|
366
|
+
async processFile(path: string, onProgress: (percent: number) => void): Promise<string> {
|
|
367
|
+
await onProgress(0);
|
|
368
|
+
// ... process file ...
|
|
369
|
+
await onProgress(50);
|
|
370
|
+
// ... more processing ...
|
|
371
|
+
await onProgress(100);
|
|
372
|
+
return `Processed: ${path}`;
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
})
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Call Service with Inline Callbacks
|
|
379
|
+
|
|
380
|
+
Consume services from other plugins:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
handler: async (input, ctx) => {
|
|
384
|
+
// Create proxy to another plugin's service
|
|
385
|
+
const processor = await ctx.rpc.createProxy<{
|
|
386
|
+
processFile(path: string, onProgress: (p: number) => void): Promise<string>
|
|
387
|
+
}>('fileProcessor');
|
|
388
|
+
|
|
389
|
+
// Pass callback - it just works!
|
|
390
|
+
const result = await processor.processFile('/file.txt', (progress) => {
|
|
391
|
+
ctx.logger.info(`Progress: ${progress}%`);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return { result };
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Explicit Callbacks with Cleanup
|
|
399
|
+
|
|
400
|
+
For long-lived callbacks (subscriptions, event listeners), use `createCallback()`:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
handler: async (input, ctx) => {
|
|
404
|
+
// Auto-cleanup after 10 calls
|
|
405
|
+
const callback = await ctx.rpc.createCallback!(
|
|
406
|
+
(event) => ctx.logger.info('Event:', event),
|
|
407
|
+
{ maxCalls: 10 }
|
|
408
|
+
);
|
|
409
|
+
await eventService.subscribe(callback);
|
|
410
|
+
|
|
411
|
+
// Auto-cleanup after 5 seconds
|
|
412
|
+
const callback2 = await ctx.rpc.createCallback!(
|
|
413
|
+
(data) => ctx.logger.info('Data:', data),
|
|
414
|
+
{ timeout: 5000 }
|
|
415
|
+
);
|
|
416
|
+
await dataStream.subscribe(callback2);
|
|
417
|
+
|
|
418
|
+
// Manual cleanup
|
|
419
|
+
const callback3 = await ctx.rpc.createCallback!((msg) => ctx.logger.info(msg));
|
|
420
|
+
// ... later
|
|
421
|
+
ctx.rpc.cleanupCallback!(callback3);
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**For comprehensive RPC patterns and best practices:** Run `npx @majkapp/plugin-kit --rpc`
|
|
426
|
+
|
|
354
427
|
## Complete Example: Dashboard Data
|
|
355
428
|
|
|
356
429
|
```typescript
|
|
@@ -495,6 +568,7 @@ test('getDashboardData aggregates MAJK data', async () => {
|
|
|
495
568
|
|
|
496
569
|
## Next Steps
|
|
497
570
|
|
|
571
|
+
Run `npx @majkapp/plugin-kit --rpc` - Inter-plugin communication with callbacks
|
|
498
572
|
Run `npx @majkapp/plugin-kit --lifecycle` - onReady and event subscriptions
|
|
499
573
|
Run `npx @majkapp/plugin-kit --services` - Group functions into services
|
|
500
574
|
Run `npx @majkapp/plugin-kit --testing` - Test functions with mock context
|
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
# RPC Callbacks
|
|
2
|
+
|
|
3
|
+
RPC callbacks allow you to pass callback functions across plugin boundaries. The MAJK RPC system automatically handles callback serialization, invocation, and cleanup.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
When building plugins that communicate via RPC, you can now pass callback functions as arguments. The system automatically:
|
|
8
|
+
- **Wraps callbacks** into serializable handles
|
|
9
|
+
- **Registers them** as temporary RPC services
|
|
10
|
+
- **Invokes them** when called from the remote side
|
|
11
|
+
- **Cleans them up** based on your chosen strategy
|
|
12
|
+
|
|
13
|
+
## Basic Usage (Inline Callbacks)
|
|
14
|
+
|
|
15
|
+
The simplest way to use callbacks is to pass them directly as function arguments. These have **no automatic cleanup** by default.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Plugin A: Register a service that accepts callbacks
|
|
19
|
+
const plugin = definePlugin('file-processor', 'File Processor', '1.0.0')
|
|
20
|
+
.pluginRoot(__dirname)
|
|
21
|
+
.onLoad(async (ctx) => {
|
|
22
|
+
// Register service with callback parameter
|
|
23
|
+
await ctx.rpc.registerService('processor', {
|
|
24
|
+
async processFile(
|
|
25
|
+
filePath: string,
|
|
26
|
+
onProgress: (percent: number, message: string) => void
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
// Call the callback as the file processes
|
|
29
|
+
await onProgress(0, 'Starting...');
|
|
30
|
+
await onProgress(25, 'Reading file...');
|
|
31
|
+
await onProgress(50, 'Processing...');
|
|
32
|
+
await onProgress(75, 'Writing results...');
|
|
33
|
+
await onProgress(100, 'Complete!');
|
|
34
|
+
|
|
35
|
+
return `Processed: ${filePath}`;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
})
|
|
39
|
+
.build();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// Plugin B: Use the service with inline callback
|
|
44
|
+
const plugin = definePlugin('file-consumer', 'File Consumer', '1.0.0')
|
|
45
|
+
.pluginRoot(__dirname)
|
|
46
|
+
.onLoad(async (ctx) => {
|
|
47
|
+
// Create proxy to the remote service
|
|
48
|
+
const processor = await ctx.rpc.createProxy<{
|
|
49
|
+
processFile(path: string, onProgress: (p: number, m: string) => void): Promise<string>
|
|
50
|
+
}>('processor');
|
|
51
|
+
|
|
52
|
+
// Pass callback directly - it just works!
|
|
53
|
+
const result = await processor.processFile('/data/large-file.txt', (percent, message) => {
|
|
54
|
+
console.log(`Progress: ${percent}% - ${message}`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log(result); // "Processed: /data/large-file.txt"
|
|
58
|
+
})
|
|
59
|
+
.build();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Key Point**: Inline callbacks live forever (no automatic cleanup). This is perfect for:
|
|
63
|
+
- One-time operations (file processing, data fetching)
|
|
64
|
+
- Short-lived interactions
|
|
65
|
+
- Callbacks that will be called a known number of times
|
|
66
|
+
|
|
67
|
+
## Explicit Callbacks with Cleanup Strategies
|
|
68
|
+
|
|
69
|
+
For long-lived callbacks (subscriptions, event listeners, etc.), use `createCallback()` to manage cleanup explicitly.
|
|
70
|
+
|
|
71
|
+
### Max Calls Strategy
|
|
72
|
+
|
|
73
|
+
Auto-cleanup after N invocations:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Plugin A: Event emitter service
|
|
77
|
+
await ctx.rpc.registerService('events', {
|
|
78
|
+
async subscribe(callback: (event: string) => void): Promise<void> {
|
|
79
|
+
// callback will be called multiple times
|
|
80
|
+
setInterval(() => callback('tick'), 1000);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// Plugin B: Subscribe with maxCalls limit
|
|
87
|
+
const events = await ctx.rpc.createProxy<{
|
|
88
|
+
subscribe(cb: (event: string) => void): Promise<void>
|
|
89
|
+
}>('events');
|
|
90
|
+
|
|
91
|
+
// Create callback that self-destructs after 10 calls
|
|
92
|
+
const callback = await ctx.rpc.createCallback!(
|
|
93
|
+
(event: string) => {
|
|
94
|
+
console.log('Event:', event);
|
|
95
|
+
},
|
|
96
|
+
{ maxCalls: 10 } // Auto-cleanup after 10 invocations
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
await events.subscribe(callback);
|
|
100
|
+
// After 10 events, the callback is automatically cleaned up
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Timeout Strategy
|
|
104
|
+
|
|
105
|
+
Auto-cleanup after time expires:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// Subscribe for only 5 seconds
|
|
109
|
+
const callback = await ctx.rpc.createCallback!(
|
|
110
|
+
(data: string) => {
|
|
111
|
+
console.log('Received:', data);
|
|
112
|
+
},
|
|
113
|
+
{ timeout: 5000 } // Auto-cleanup after 5 seconds
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
await dataStream.subscribe(callback);
|
|
117
|
+
// After 5 seconds, callback is cleaned up automatically
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Manual Cleanup
|
|
121
|
+
|
|
122
|
+
For complete control, clean up manually:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// Create callback without auto-cleanup strategies
|
|
126
|
+
const callback = await ctx.rpc.createCallback!((message: string) => {
|
|
127
|
+
console.log('Message:', message);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await messageService.subscribe(callback);
|
|
131
|
+
|
|
132
|
+
// Later... manually clean up when done
|
|
133
|
+
ctx.rpc.cleanupCallback!(callback);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Real-World Patterns
|
|
137
|
+
|
|
138
|
+
### Pattern 1: File Processing with Progress
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
// Service side
|
|
142
|
+
await ctx.rpc.registerService('fileOps', {
|
|
143
|
+
async batchProcess(
|
|
144
|
+
files: string[],
|
|
145
|
+
onFileComplete: (file: string, result: string) => void,
|
|
146
|
+
onError: (file: string, error: string) => void
|
|
147
|
+
): Promise<void> {
|
|
148
|
+
for (const file of files) {
|
|
149
|
+
try {
|
|
150
|
+
const result = await processFile(file);
|
|
151
|
+
await onFileComplete(file, result);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
await onError(file, error.message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Client side
|
|
160
|
+
const fileOps = await ctx.rpc.createProxy<typeof FileOpsService>('fileOps');
|
|
161
|
+
|
|
162
|
+
await fileOps.batchProcess(
|
|
163
|
+
['/file1.txt', '/file2.txt', '/file3.txt'],
|
|
164
|
+
(file, result) => {
|
|
165
|
+
console.log(`✓ ${file}: ${result}`);
|
|
166
|
+
},
|
|
167
|
+
(file, error) => {
|
|
168
|
+
console.error(`✗ ${file}: ${error}`);
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Pattern 2: Real-Time Data Subscriptions
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// Service side: Data stream
|
|
177
|
+
await ctx.rpc.registerService('dataStream', {
|
|
178
|
+
async subscribe(
|
|
179
|
+
filter: string,
|
|
180
|
+
onData: (data: any) => void
|
|
181
|
+
): Promise<() => void> {
|
|
182
|
+
const subscription = dataSource.on(filter, onData);
|
|
183
|
+
|
|
184
|
+
// Return unsubscribe function (also a callback!)
|
|
185
|
+
return () => subscription.close();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Client side
|
|
190
|
+
const stream = await ctx.rpc.createProxy<{
|
|
191
|
+
subscribe(filter: string, onData: (data: any) => void): Promise<() => void>
|
|
192
|
+
}>('dataStream');
|
|
193
|
+
|
|
194
|
+
// Subscribe with 1-minute timeout
|
|
195
|
+
const callback = await ctx.rpc.createCallback!(
|
|
196
|
+
(data: any) => {
|
|
197
|
+
console.log('New data:', data);
|
|
198
|
+
},
|
|
199
|
+
{ timeout: 60000 }
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const unsubscribe = await stream.subscribe('temperature > 100', callback);
|
|
203
|
+
|
|
204
|
+
// When done early, clean up both callback and subscription
|
|
205
|
+
unsubscribe(); // Stop receiving data
|
|
206
|
+
ctx.rpc.cleanupCallback!(callback); // Clean up callback
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Pattern 3: Filtering with Predicates
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// Service side
|
|
213
|
+
await ctx.rpc.registerService('dataService', {
|
|
214
|
+
async getFiltered(
|
|
215
|
+
items: any[],
|
|
216
|
+
predicate: (item: any) => boolean | Promise<boolean>
|
|
217
|
+
): Promise<any[]> {
|
|
218
|
+
const filtered = [];
|
|
219
|
+
for (const item of items) {
|
|
220
|
+
if (await predicate(item)) {
|
|
221
|
+
filtered.push(item);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return filtered;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Client side
|
|
229
|
+
const dataService = await ctx.rpc.createProxy<typeof DataService>('dataService');
|
|
230
|
+
|
|
231
|
+
const filtered = await dataService.getFiltered(
|
|
232
|
+
allUsers,
|
|
233
|
+
(user) => user.age > 18 && user.active
|
|
234
|
+
);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Pattern 4: Event Hooks
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// Service side: Task runner with hooks
|
|
241
|
+
await ctx.rpc.registerService('taskRunner', {
|
|
242
|
+
async runTask(
|
|
243
|
+
taskName: string,
|
|
244
|
+
hooks: {
|
|
245
|
+
onStart?: () => void,
|
|
246
|
+
onProgress?: (percent: number) => void,
|
|
247
|
+
onComplete?: (result: any) => void,
|
|
248
|
+
onError?: (error: string) => void
|
|
249
|
+
}
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
try {
|
|
252
|
+
await hooks.onStart?.();
|
|
253
|
+
|
|
254
|
+
// Execute task with progress updates
|
|
255
|
+
for (let i = 0; i <= 100; i += 10) {
|
|
256
|
+
await hooks.onProgress?.(i);
|
|
257
|
+
await simulateWork();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const result = await finalizeTask();
|
|
261
|
+
await hooks.onComplete?.(result);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
await hooks.onError?.(error.message);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Client side
|
|
269
|
+
const taskRunner = await ctx.rpc.createProxy<typeof TaskRunner>('taskRunner');
|
|
270
|
+
|
|
271
|
+
await taskRunner.runTask('compile-project', {
|
|
272
|
+
onStart: () => console.log('Starting compilation...'),
|
|
273
|
+
onProgress: (p) => console.log(`Progress: ${p}%`),
|
|
274
|
+
onComplete: (result) => console.log('Done!', result),
|
|
275
|
+
onError: (error) => console.error('Failed:', error)
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Error Handling
|
|
280
|
+
|
|
281
|
+
### Callback Cleaned Up Error
|
|
282
|
+
|
|
283
|
+
If a callback is invoked after cleanup, a `CallbackCleanedUpError` is thrown:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const callback = await ctx.rpc.createCallback!(
|
|
287
|
+
(data) => console.log(data),
|
|
288
|
+
{ maxCalls: 1 }
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
await service.subscribe(callback);
|
|
292
|
+
// After 1 call, callback is cleaned up
|
|
293
|
+
|
|
294
|
+
// If service tries to call it again:
|
|
295
|
+
// Error: Callback xyz has been cleaned up and is no longer available.
|
|
296
|
+
// This can happen if: 1) maxCalls limit was reached,
|
|
297
|
+
// 2) timeout expired,
|
|
298
|
+
// 3) cleanupCallback() was called manually.
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Handling Remote Errors
|
|
302
|
+
|
|
303
|
+
Errors thrown in callbacks propagate back to the caller:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// Service side
|
|
307
|
+
await service.process(
|
|
308
|
+
files,
|
|
309
|
+
async (file) => {
|
|
310
|
+
if (!file.exists()) {
|
|
311
|
+
throw new Error(`File not found: ${file.path}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
// Error will be received by service with the message
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Robust Error Handling Pattern
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// Service side
|
|
322
|
+
async processWithCallback(
|
|
323
|
+
data: any[],
|
|
324
|
+
callback: (item: any) => Promise<void>
|
|
325
|
+
): Promise<{ success: any[], errors: any[] }> {
|
|
326
|
+
const success = [];
|
|
327
|
+
const errors = [];
|
|
328
|
+
|
|
329
|
+
for (const item of data) {
|
|
330
|
+
try {
|
|
331
|
+
await callback(item);
|
|
332
|
+
success.push(item);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
errors.push({ item, error: error.message });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { success, errors };
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Monitoring and Debugging
|
|
343
|
+
|
|
344
|
+
### Get Callback Statistics
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
const stats = ctx.rpc.getCallbackStats!();
|
|
348
|
+
console.log('Active callbacks:', stats.activeCallbacks);
|
|
349
|
+
console.log('Inline callbacks:', stats.inlineCallbacks);
|
|
350
|
+
console.log('Explicit callbacks:', stats.explicitCallbacks);
|
|
351
|
+
console.log('Total created:', stats.totalCreated);
|
|
352
|
+
console.log('Total cleaned:', stats.totalCleaned);
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Statistics breakdown**:
|
|
356
|
+
- `activeCallbacks`: Currently registered callbacks
|
|
357
|
+
- `inlineCallbacks`: Auto-wrapped inline callbacks (no cleanup)
|
|
358
|
+
- `explicitCallbacks`: Explicitly created via `createCallback()`
|
|
359
|
+
- `totalCreated`: Lifetime count of callbacks created
|
|
360
|
+
- `totalCleaned`: Lifetime count of callbacks cleaned up
|
|
361
|
+
|
|
362
|
+
### Debugging Tips
|
|
363
|
+
|
|
364
|
+
1. **Use timeouts for testing**: When developing, use short timeouts to test cleanup behavior:
|
|
365
|
+
```typescript
|
|
366
|
+
const callback = await ctx.rpc.createCallback!(fn, { timeout: 5000 });
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
2. **Monitor stats**: Check stats periodically to detect callback leaks:
|
|
370
|
+
```typescript
|
|
371
|
+
setInterval(() => {
|
|
372
|
+
const stats = ctx.rpc.getCallbackStats!();
|
|
373
|
+
if (stats.activeCallbacks > 100) {
|
|
374
|
+
console.warn('High callback count - possible leak');
|
|
375
|
+
}
|
|
376
|
+
}, 60000);
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
3. **Log cleanup**: Add logging to understand cleanup timing:
|
|
380
|
+
```typescript
|
|
381
|
+
const callback = await ctx.rpc.createCallback!(
|
|
382
|
+
(data) => {
|
|
383
|
+
console.log('Callback invoked:', data);
|
|
384
|
+
},
|
|
385
|
+
{ maxCalls: 10 }
|
|
386
|
+
);
|
|
387
|
+
console.log('Created callback:', callback.callbackId);
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## Best Practices
|
|
391
|
+
|
|
392
|
+
### ✅ DO
|
|
393
|
+
|
|
394
|
+
- **Use inline callbacks for simple, one-time operations**
|
|
395
|
+
```typescript
|
|
396
|
+
await service.processFile(file, (progress) => console.log(progress));
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
- **Use `createCallback()` with `maxCalls` for subscriptions**
|
|
400
|
+
```typescript
|
|
401
|
+
const cb = await ctx.rpc.createCallback!(handler, { maxCalls: 100 });
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
- **Use `createCallback()` with `timeout` for time-limited operations**
|
|
405
|
+
```typescript
|
|
406
|
+
const cb = await ctx.rpc.createCallback!(handler, { timeout: 30000 });
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
- **Clean up manually when you control the lifecycle**
|
|
410
|
+
```typescript
|
|
411
|
+
const cb = await ctx.rpc.createCallback!(handler);
|
|
412
|
+
// ... use callback ...
|
|
413
|
+
ctx.rpc.cleanupCallback!(cb);
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- **Handle errors in callbacks gracefully**
|
|
417
|
+
```typescript
|
|
418
|
+
(data) => {
|
|
419
|
+
try {
|
|
420
|
+
processData(data);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error('Callback error:', error);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### ❌ DON'T
|
|
428
|
+
|
|
429
|
+
- **Don't use inline callbacks for long-lived subscriptions**
|
|
430
|
+
```typescript
|
|
431
|
+
// ❌ Bad: No cleanup, callback lives forever
|
|
432
|
+
await eventStream.subscribe((event) => console.log(event));
|
|
433
|
+
|
|
434
|
+
// ✅ Good: Use timeout or manual cleanup
|
|
435
|
+
const cb = await ctx.rpc.createCallback!(
|
|
436
|
+
(event) => console.log(event),
|
|
437
|
+
{ timeout: 60000 }
|
|
438
|
+
);
|
|
439
|
+
await eventStream.subscribe(cb);
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
- **Don't forget to clean up when lifecycle is known**
|
|
443
|
+
```typescript
|
|
444
|
+
// ❌ Bad: No cleanup on plugin unload
|
|
445
|
+
await ctx.rpc.createCallback!(handler);
|
|
446
|
+
|
|
447
|
+
// ✅ Good: Track and clean up
|
|
448
|
+
const callbacks = [];
|
|
449
|
+
callbacks.push(await ctx.rpc.createCallback!(handler));
|
|
450
|
+
|
|
451
|
+
// In onUnload:
|
|
452
|
+
callbacks.forEach(cb => ctx.rpc.cleanupCallback!(cb));
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
- **Don't pass complex objects through callbacks**
|
|
456
|
+
```typescript
|
|
457
|
+
// ❌ Bad: Large objects, circular references
|
|
458
|
+
callback({ largeData: hugeArray, circular: someObjectWithCircularRefs });
|
|
459
|
+
|
|
460
|
+
// ✅ Good: Pass only necessary data
|
|
461
|
+
callback({ id: item.id, status: item.status });
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Type Safety
|
|
465
|
+
|
|
466
|
+
All callbacks maintain full TypeScript type safety:
|
|
467
|
+
|
|
468
|
+
```typescript
|
|
469
|
+
// Service definition with typed callbacks
|
|
470
|
+
interface FileService {
|
|
471
|
+
processFile(
|
|
472
|
+
path: string,
|
|
473
|
+
onProgress: (percent: number, message: string) => void,
|
|
474
|
+
onError: (error: Error) => void
|
|
475
|
+
): Promise<string>;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Client gets full type checking
|
|
479
|
+
const service = await ctx.rpc.createProxy<FileService>('fileService');
|
|
480
|
+
|
|
481
|
+
await service.processFile(
|
|
482
|
+
'/file.txt',
|
|
483
|
+
(percent, message) => {
|
|
484
|
+
// 'percent' is number, 'message' is string - fully typed!
|
|
485
|
+
},
|
|
486
|
+
(error) => {
|
|
487
|
+
// 'error' is Error - fully typed!
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Backward Compatibility
|
|
493
|
+
|
|
494
|
+
The callback feature is **100% backward compatible**:
|
|
495
|
+
- All callback methods are optional (`createCallback?`, `cleanupCallback?`, `getCallbackStats?`)
|
|
496
|
+
- Existing plugins work unchanged
|
|
497
|
+
- RPC calls without callbacks work exactly as before
|
|
498
|
+
- No breaking changes to existing APIs
|
|
499
|
+
|
|
500
|
+
## Limitations
|
|
501
|
+
|
|
502
|
+
1. **Serialization**: Callbacks must not capture complex objects or closures that reference non-serializable data
|
|
503
|
+
2. **Network boundaries**: Callbacks work within the same MAJK process (in-process RPC)
|
|
504
|
+
3. **Performance**: Each callback creates a temporary RPC service - avoid creating thousands at once
|
|
505
|
+
4. **Memory**: Inline callbacks without cleanup will remain in memory until the bridge is destroyed
|
|
506
|
+
|
|
507
|
+
## Summary
|
|
508
|
+
|
|
509
|
+
RPC callbacks make inter-plugin communication natural and intuitive:
|
|
510
|
+
|
|
511
|
+
| Use Case | Strategy | Example |
|
|
512
|
+
|----------|----------|---------|
|
|
513
|
+
| One-time operations | Inline callback | `process(file, (progress) => ...)` |
|
|
514
|
+
| N-limited subscriptions | `maxCalls` | `createCallback(fn, { maxCalls: 10 })` |
|
|
515
|
+
| Time-limited operations | `timeout` | `createCallback(fn, { timeout: 5000 })` |
|
|
516
|
+
| Controlled lifecycle | Manual cleanup | `createCallback(fn)` + `cleanupCallback()` |
|
|
517
|
+
|
|
518
|
+
Choose the right strategy for your use case, and let the RPC system handle the rest!
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@majkapp/plugin-kit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.1",
|
|
4
4
|
"description": "Pure plugin definition library for MAJK - outputs plugin definitions, not HTTP servers",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,12 +27,16 @@
|
|
|
27
27
|
"build": "tsc && cp src/mcp-dom-agent.js dist/",
|
|
28
28
|
"watch": "tsc --watch",
|
|
29
29
|
"clean": "rm -rf dist",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"test:ui": "vitest --ui",
|
|
30
33
|
"promptable": "node ./bin/promptable-cli.js --llm",
|
|
31
34
|
"promptable:functions": "node ./bin/promptable-cli.js --functions",
|
|
32
35
|
"promptable:screens": "node ./bin/promptable-cli.js --screens",
|
|
33
36
|
"promptable:hooks": "node ./bin/promptable-cli.js --hooks",
|
|
34
37
|
"promptable:context": "node ./bin/promptable-cli.js --context",
|
|
35
38
|
"promptable:services": "node ./bin/promptable-cli.js --services",
|
|
39
|
+
"promptable:rpc": "node ./bin/promptable-cli.js --rpc",
|
|
36
40
|
"promptable:lifecycle": "node ./bin/promptable-cli.js --lifecycle",
|
|
37
41
|
"promptable:testing": "node ./bin/promptable-cli.js --testing",
|
|
38
42
|
"promptable:config": "node ./bin/promptable-cli.js --config",
|
|
@@ -53,6 +57,8 @@
|
|
|
53
57
|
},
|
|
54
58
|
"devDependencies": {
|
|
55
59
|
"@types/node": "^20.19.25",
|
|
56
|
-
"
|
|
60
|
+
"@vitest/ui": "^4.0.12",
|
|
61
|
+
"typescript": "^5.9.3",
|
|
62
|
+
"vitest": "^4.0.12"
|
|
57
63
|
}
|
|
58
64
|
}
|
package/dist/transports.d.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type { Transport, TransportMetadata, FunctionRegistry, PluginContext, RequestLike, ResponseLike } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* HTTP Transport configuration
|
|
4
|
-
*/
|
|
5
|
-
export interface HttpTransportConfig {
|
|
6
|
-
basePath?: string;
|
|
7
|
-
cors?: boolean;
|
|
8
|
-
validation?: {
|
|
9
|
-
request?: boolean;
|
|
10
|
-
response?: boolean;
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* HTTP Transport implementation
|
|
15
|
-
*/
|
|
16
|
-
export declare class HttpTransport implements Transport {
|
|
17
|
-
name: string;
|
|
18
|
-
private registry?;
|
|
19
|
-
private context?;
|
|
20
|
-
private config;
|
|
21
|
-
constructor(config?: HttpTransportConfig);
|
|
22
|
-
initialize(registry: FunctionRegistry, context: PluginContext): Promise<void>;
|
|
23
|
-
start(): Promise<void>;
|
|
24
|
-
stop(): Promise<void>;
|
|
25
|
-
getMetadata(): TransportMetadata;
|
|
26
|
-
/**
|
|
27
|
-
* Handle a function call via HTTP
|
|
28
|
-
*/
|
|
29
|
-
handleFunctionCall(functionName: string, req: RequestLike, res: ResponseLike, ctx: any): Promise<void>;
|
|
30
|
-
/**
|
|
31
|
-
* Get discovery information
|
|
32
|
-
*/
|
|
33
|
-
getDiscovery(): any;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* WebSocket Transport (placeholder for future implementation)
|
|
37
|
-
*/
|
|
38
|
-
export declare class WebSocketTransport implements Transport {
|
|
39
|
-
name: string;
|
|
40
|
-
private config;
|
|
41
|
-
constructor(config?: any);
|
|
42
|
-
initialize(registry: FunctionRegistry, context: PluginContext): Promise<void>;
|
|
43
|
-
start(): Promise<void>;
|
|
44
|
-
stop(): Promise<void>;
|
|
45
|
-
getMetadata(): TransportMetadata;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* MCP Transport (placeholder for future implementation)
|
|
49
|
-
*/
|
|
50
|
-
export declare class MCPTransport implements Transport {
|
|
51
|
-
name: string;
|
|
52
|
-
private config;
|
|
53
|
-
constructor(config?: any);
|
|
54
|
-
initialize(registry: FunctionRegistry, context: PluginContext): Promise<void>;
|
|
55
|
-
start(): Promise<void>;
|
|
56
|
-
stop(): Promise<void>;
|
|
57
|
-
getMetadata(): TransportMetadata;
|
|
58
|
-
}
|
|
59
|
-
//# sourceMappingURL=transports.d.ts.map
|
package/dist/transports.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"transports.d.ts","sourceRoot":"","sources":["../src/transports.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,YAAY,EACb,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE;QACX,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;KACpB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,aAAc,YAAW,SAAS;IAC7C,IAAI,SAAU;IACd,OAAO,CAAC,QAAQ,CAAC,CAAmB;IACpC,OAAO,CAAC,OAAO,CAAC,CAAgB;IAChC,OAAO,CAAC,MAAM,CAAsB;gBAExB,MAAM,GAAE,mBAAwB;IAWtC,UAAU,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAM7E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAKtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,WAAW,IAAI,iBAAiB;IAUhC;;OAEG;IACG,kBAAkB,CACtB,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,YAAY,EACjB,GAAG,EAAE,GAAG,GACP,OAAO,CAAC,IAAI,CAAC;IAgDhB;;OAEG;IACH,YAAY,IAAI,GAAG;CAkCpB;AAED;;GAEG;AACH,qBAAa,kBAAmB,YAAW,SAAS;IAClD,IAAI,SAAe;IACnB,OAAO,CAAC,MAAM,CAAM;gBAER,MAAM,GAAE,GAAQ;IAItB,UAAU,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,WAAW,IAAI,iBAAiB;CAMjC;AAED;;GAEG;AACH,qBAAa,YAAa,YAAW,SAAS;IAC5C,IAAI,SAAS;IACb,OAAO,CAAC,MAAM,CAAM;gBAER,MAAM,GAAE,GAAQ;IAItB,UAAU,CAAC,QAAQ,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,WAAW,IAAI,iBAAiB;CAMjC"}
|