@majkapp/plugin-kit 3.5.4 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -0
- package/bin/promptable-cli.js +12 -0
- package/dist/generator/cli.js +0 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/majk-interface-types.d.ts +395 -0
- package/dist/majk-interface-types.d.ts.map +1 -1
- package/dist/majk-interface-types.js +15 -0
- package/dist/transports.d.ts +59 -0
- package/dist/transports.d.ts.map +1 -0
- package/dist/transports.js +171 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/AI.md +830 -0
- package/docs/API.md +8 -0
- package/docs/CONTEXT.md +74 -0
- package/docs/FULL.md +148 -6
- package/docs/INDEX.md +1 -0
- package/docs/RPC_CALLBACKS.md +518 -0
- package/package.json +2 -1
|
@@ -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.0",
|
|
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",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"promptable:hooks": "node ./bin/promptable-cli.js --hooks",
|
|
34
34
|
"promptable:context": "node ./bin/promptable-cli.js --context",
|
|
35
35
|
"promptable:services": "node ./bin/promptable-cli.js --services",
|
|
36
|
+
"promptable:rpc": "node ./bin/promptable-cli.js --rpc",
|
|
36
37
|
"promptable:lifecycle": "node ./bin/promptable-cli.js --lifecycle",
|
|
37
38
|
"promptable:testing": "node ./bin/promptable-cli.js --testing",
|
|
38
39
|
"promptable:config": "node ./bin/promptable-cli.js --config",
|