@majkapp/plugin-kit 3.2.1 → 3.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.
@@ -0,0 +1,490 @@
1
+ # Lifecycle
2
+
3
+ Plugins have a lifecycle with setup and teardown phases. Use `.onReady()` for initialization.
4
+
5
+ ## onReady
6
+
7
+ Called once when the plugin is loaded. Use for subscriptions, timers, and async setup.
8
+
9
+ ```typescript
10
+ // src/index.ts
11
+ import { definePlugin } from '@majkapp/plugin-kit';
12
+
13
+ const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
14
+ .pluginRoot(__dirname)
15
+
16
+ // Define functions first
17
+ .function('getEvents', { /* ... */ })
18
+ .function('clearEvents', { /* ... */ })
19
+
20
+ // Lifecycle hook - called when plugin loads
21
+ .onReady(async (ctx, cleanup) => {
22
+ ctx.logger.info('Plugin initializing...');
23
+
24
+ // Perform async setup
25
+ await initializePluginData(ctx);
26
+
27
+ // Register cleanup (called when plugin unloads/reloads)
28
+ cleanup(() => {
29
+ ctx.logger.info('Plugin cleaning up');
30
+ // Cleanup code here
31
+ });
32
+
33
+ ctx.logger.info('Plugin ready');
34
+ })
35
+
36
+ .build();
37
+ ```
38
+
39
+ ## Event Bus Subscription (Real Example)
40
+
41
+ From `samples/full-featured/src/index.ts`:
42
+
43
+ ```typescript
44
+ // In-memory event log (scoped to plugin instance)
45
+ const eventLog: any[] = [];
46
+ const maxEvents = 1000;
47
+
48
+ const plugin = definePlugin('full-featured', 'Full Featured', '1.0.0')
49
+ .pluginRoot(__dirname)
50
+
51
+ // Functions that access eventLog
52
+ .function('getEvents', {
53
+ description: 'Get all logged events from the event bus',
54
+ input: { /* ... */ },
55
+ output: { /* ... */ },
56
+ handler: async (input, ctx) => {
57
+ return {
58
+ events: eventLog,
59
+ count: eventLog.length
60
+ };
61
+ }
62
+ })
63
+
64
+ .function('clearEvents', {
65
+ description: 'Clear the event log',
66
+ input: { /* ... */ },
67
+ output: { /* ... */ },
68
+ handler: async (input, ctx) => {
69
+ const count = eventLog.length;
70
+ eventLog.length = 0; // Clear array
71
+ ctx.logger.info(`Cleared ${count} events`);
72
+ return {
73
+ success: true,
74
+ clearedCount: count
75
+ };
76
+ }
77
+ })
78
+
79
+ // Subscribe to events when plugin loads
80
+ .onReady(async (ctx, cleanup) => {
81
+ ctx.logger.info('Full Featured plugin ready - subscribing to all events');
82
+
83
+ // Subscribe to ALL events from MAJK event bus
84
+ // CRITICAL: Must await the subscription (returns Promise<Subscription>)
85
+ const subscription = await ctx.majk.eventBus.subscribeAll((event: any) => {
86
+ // Build a log entry for each event
87
+ const logEntry = {
88
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
89
+ timestamp: new Date().toISOString(),
90
+ entityType: event.entityType, // 'conversation', 'todo', 'project', etc.
91
+ eventType: event.type, // 'created', 'updated', 'deleted'
92
+ entityId: event.entity?.id || 'unknown',
93
+ entitySummary: event.entity ? {
94
+ ...( event.entity.name && { name: event.entity.name }),
95
+ ...( event.entity.title && { title: event.entity.title }),
96
+ ...( event.entity.status && { status: event.entity.status })
97
+ } : {},
98
+ metadata: event.metadata || {},
99
+ fullEntity: event.entity
100
+ };
101
+
102
+ // Add to front of array (newest first), enforce max size
103
+ eventLog.unshift(logEntry);
104
+ if (eventLog.length > maxEvents) {
105
+ eventLog.length = maxEvents; // Truncate old events
106
+ }
107
+
108
+ ctx.logger.info(`Event logged: ${event.entityType}.${event.type} - Total: ${eventLog.length}`);
109
+ });
110
+
111
+ // ALWAYS register cleanup for subscriptions, timers, etc.
112
+ // This runs when plugin is disabled or reloaded
113
+ cleanup(() => {
114
+ if (subscription && typeof subscription.unsubscribe === 'function') {
115
+ subscription.unsubscribe();
116
+ ctx.logger.info('Unsubscribed from EventBus');
117
+ }
118
+ });
119
+ })
120
+
121
+ .build();
122
+ ```
123
+
124
+ ## Timer Example
125
+
126
+ ```typescript
127
+ .onReady(async (ctx, cleanup) => {
128
+ ctx.logger.info('Starting periodic task');
129
+
130
+ // Start interval timer
131
+ const intervalId = setInterval(async () => {
132
+ try {
133
+ // Perform periodic task
134
+ const stats = await calculateStats(ctx);
135
+ await ctx.storage.set('last-stats', stats);
136
+ ctx.logger.info('Stats updated', stats);
137
+ } catch (error) {
138
+ ctx.logger.error('Periodic task failed', { error: error.message });
139
+ }
140
+ }, 60000); // Every 60 seconds
141
+
142
+ // Clean up timer on unload
143
+ cleanup(() => {
144
+ clearInterval(intervalId);
145
+ ctx.logger.info('Stopped periodic task');
146
+ });
147
+ })
148
+ ```
149
+
150
+ ## Initialize Storage
151
+
152
+ ```typescript
153
+ .onReady(async (ctx, cleanup) => {
154
+ ctx.logger.info('Initializing plugin storage');
155
+
156
+ // Check if first run
157
+ const initialized = await ctx.storage.get('initialized');
158
+
159
+ if (!initialized) {
160
+ // First-time setup
161
+ await ctx.storage.set('initialized', true);
162
+ await ctx.storage.set('sessions', []);
163
+ await ctx.storage.set('settings', {
164
+ theme: 'light',
165
+ notifications: true
166
+ });
167
+
168
+ ctx.logger.info('Plugin storage initialized');
169
+ } else {
170
+ ctx.logger.info('Plugin storage already initialized');
171
+ }
172
+
173
+ // No cleanup needed for storage initialization
174
+ })
175
+ ```
176
+
177
+ ## Multiple Cleanups
178
+
179
+ ```typescript
180
+ .onReady(async (ctx, cleanup) => {
181
+ // Setup 1: Event subscription
182
+ const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
183
+ // Handle event
184
+ });
185
+
186
+ cleanup(() => {
187
+ subscription.unsubscribe();
188
+ ctx.logger.info('Unsubscribed from events');
189
+ });
190
+
191
+ // Setup 2: Timer
192
+ const timerId = setInterval(() => {
193
+ // Periodic task
194
+ }, 30000);
195
+
196
+ cleanup(() => {
197
+ clearInterval(timerId);
198
+ ctx.logger.info('Stopped timer');
199
+ });
200
+
201
+ // Setup 3: External connection
202
+ const connection = await connectToExternalService();
203
+
204
+ cleanup(async () => {
205
+ await connection.disconnect();
206
+ ctx.logger.info('Disconnected from external service');
207
+ });
208
+
209
+ ctx.logger.info('Plugin fully initialized');
210
+ })
211
+ ```
212
+
213
+ ## Error Handling in onReady
214
+
215
+ ```typescript
216
+ .onReady(async (ctx, cleanup) => {
217
+ try {
218
+ ctx.logger.info('Plugin initializing...');
219
+
220
+ // Attempt initialization
221
+ const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
222
+ // Handle event
223
+ });
224
+
225
+ cleanup(() => {
226
+ subscription.unsubscribe();
227
+ });
228
+
229
+ ctx.logger.info('Plugin initialized successfully');
230
+
231
+ } catch (error) {
232
+ ctx.logger.error('Plugin initialization failed', {
233
+ error: error.message,
234
+ stack: error.stack
235
+ });
236
+
237
+ // Plugin will still load, but initialization failed
238
+ // Consider storing error state for functions to check
239
+ await ctx.storage.set('init-error', error.message);
240
+ }
241
+ })
242
+ ```
243
+
244
+ ## When to Use onReady
245
+
246
+ **Use onReady for:**
247
+ - Event bus subscriptions
248
+ - Timers and intervals
249
+ - External service connections
250
+ - Background workers
251
+ - Initial data loading
252
+ - Storage initialization
253
+
254
+ **Don't use onReady for:**
255
+ - Simple function definitions (no setup needed)
256
+ - Synchronous operations (use functions instead)
257
+ - Operations that should run per-request (use function handlers)
258
+
259
+ ## Cleanup Patterns
260
+
261
+ ### Pattern 1: Subscription Cleanup
262
+
263
+ ```typescript
264
+ cleanup(() => {
265
+ if (subscription && typeof subscription.unsubscribe === 'function') {
266
+ subscription.unsubscribe();
267
+ ctx.logger.info('Unsubscribed');
268
+ }
269
+ });
270
+ ```
271
+
272
+ ### Pattern 2: Timer Cleanup
273
+
274
+ ```typescript
275
+ cleanup(() => {
276
+ clearInterval(intervalId);
277
+ clearTimeout(timeoutId);
278
+ ctx.logger.info('Timers cleared');
279
+ });
280
+ ```
281
+
282
+ ### Pattern 3: Async Cleanup
283
+
284
+ ```typescript
285
+ cleanup(async () => {
286
+ await connection.close();
287
+ await saveState(ctx);
288
+ ctx.logger.info('Async cleanup complete');
289
+ });
290
+ ```
291
+
292
+ ### Pattern 4: Resource Cleanup
293
+
294
+ ```typescript
295
+ cleanup(() => {
296
+ try {
297
+ resource.release();
298
+ ctx.logger.info('Resource released');
299
+ } catch (error) {
300
+ ctx.logger.error('Cleanup failed', { error: error.message });
301
+ }
302
+ });
303
+ ```
304
+
305
+ ## Testing with onReady
306
+
307
+ onReady is tested through integration tests:
308
+
309
+ ```typescript
310
+ // tests/plugin/integration/lifecycle.test.js
311
+ import { test } from '@majkapp/plugin-test';
312
+ import { loadPlugin, unloadPlugin } from '@majkapp/plugin-test';
313
+ import assert from 'assert';
314
+
315
+ test('plugin initializes event subscription', async () => {
316
+ const plugin = await loadPlugin();
317
+
318
+ // Trigger an event
319
+ await triggerTestEvent({ type: 'test', data: {} });
320
+
321
+ // Wait briefly for event to be processed
322
+ await new Promise(resolve => setTimeout(resolve, 100));
323
+
324
+ // Verify event was logged
325
+ const result = await invoke('getEvents', {}, { context: plugin.context });
326
+ assert.ok(result.events.length > 0);
327
+
328
+ // Cleanup
329
+ await unloadPlugin(plugin);
330
+ });
331
+ ```
332
+
333
+ ## Best Practices
334
+
335
+ ### 1. Always Register Cleanup
336
+
337
+ ```typescript
338
+ // Good: Cleanup registered
339
+ .onReady(async (ctx, cleanup) => {
340
+ const subscription = await ctx.majk.eventBus.subscribeAll(handler);
341
+
342
+ cleanup(() => {
343
+ subscription.unsubscribe();
344
+ });
345
+ })
346
+
347
+ // Bad: No cleanup - memory leak!
348
+ .onReady(async (ctx, cleanup) => {
349
+ const subscription = await ctx.majk.eventBus.subscribeAll(handler);
350
+ // Missing cleanup!
351
+ })
352
+ ```
353
+
354
+ ### 2. Log Initialization Steps
355
+
356
+ ```typescript
357
+ .onReady(async (ctx, cleanup) => {
358
+ ctx.logger.info('Plugin initializing...');
359
+
360
+ const subscription = await ctx.majk.eventBus.subscribeAll(handler);
361
+ ctx.logger.info('Event subscription created');
362
+
363
+ cleanup(() => {
364
+ subscription.unsubscribe();
365
+ ctx.logger.info('Event subscription cleaned up');
366
+ });
367
+
368
+ ctx.logger.info('Plugin initialization complete');
369
+ })
370
+ ```
371
+
372
+ ### 3. Handle Errors Gracefully
373
+
374
+ ```typescript
375
+ .onReady(async (ctx, cleanup) => {
376
+ try {
377
+ const subscription = await ctx.majk.eventBus.subscribeAll(handler);
378
+
379
+ cleanup(() => {
380
+ try {
381
+ subscription.unsubscribe();
382
+ } catch (error) {
383
+ ctx.logger.error('Cleanup error', { error: error.message });
384
+ }
385
+ });
386
+ } catch (error) {
387
+ ctx.logger.error('Initialization error', { error: error.message });
388
+ // Store error state for functions to check
389
+ await ctx.storage.set('init-error', error.message);
390
+ }
391
+ })
392
+ ```
393
+
394
+ ## Complete Example
395
+
396
+ ```typescript
397
+ // src/index.ts
398
+ import { definePlugin } from '@majkapp/plugin-kit';
399
+
400
+ const eventLog: any[] = [];
401
+ const maxEvents = 500;
402
+ let statsTimer: NodeJS.Timeout;
403
+
404
+ const plugin = definePlugin('my-plugin', 'My Plugin', '1.0.0')
405
+ .pluginRoot(__dirname)
406
+
407
+ .function('getEvents', {
408
+ description: 'Get logged events',
409
+ input: { /* ... */ },
410
+ output: { /* ... */ },
411
+ handler: async () => ({ events: eventLog })
412
+ })
413
+
414
+ .function('getStats', {
415
+ description: 'Get cached statistics',
416
+ input: { /* ... */ },
417
+ output: { /* ... */ },
418
+ handler: async (input, ctx) => {
419
+ return await ctx.storage.get('cached-stats') || { count: 0 };
420
+ }
421
+ })
422
+
423
+ .onReady(async (ctx, cleanup) => {
424
+ ctx.logger.info('My Plugin initializing...');
425
+
426
+ // 1. Initialize storage
427
+ const initialized = await ctx.storage.get('initialized');
428
+ if (!initialized) {
429
+ await ctx.storage.set('initialized', true);
430
+ await ctx.storage.set('cached-stats', { count: 0, timestamp: Date.now() });
431
+ ctx.logger.info('Storage initialized');
432
+ }
433
+
434
+ // 2. Subscribe to events
435
+ try {
436
+ const subscription = await ctx.majk.eventBus.subscribeAll((event) => {
437
+ eventLog.unshift({
438
+ id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
439
+ timestamp: new Date().toISOString(),
440
+ type: event.type,
441
+ entityType: event.entityType
442
+ });
443
+
444
+ if (eventLog.length > maxEvents) {
445
+ eventLog.length = maxEvents;
446
+ }
447
+ });
448
+
449
+ cleanup(() => {
450
+ subscription.unsubscribe();
451
+ ctx.logger.info('Event subscription cleaned up');
452
+ });
453
+
454
+ ctx.logger.info('Event subscription active');
455
+ } catch (error) {
456
+ ctx.logger.error('Event subscription failed', { error: error.message });
457
+ }
458
+
459
+ // 3. Start periodic stats update
460
+ statsTimer = setInterval(async () => {
461
+ try {
462
+ const stats = {
463
+ count: eventLog.length,
464
+ timestamp: Date.now()
465
+ };
466
+ await ctx.storage.set('cached-stats', stats);
467
+ ctx.logger.debug('Stats updated', stats);
468
+ } catch (error) {
469
+ ctx.logger.error('Stats update failed', { error: error.message });
470
+ }
471
+ }, 60000); // Every 60 seconds
472
+
473
+ cleanup(() => {
474
+ clearInterval(statsTimer);
475
+ ctx.logger.info('Stats timer stopped');
476
+ });
477
+
478
+ ctx.logger.info('My Plugin ready');
479
+ })
480
+
481
+ .build();
482
+
483
+ export = plugin;
484
+ ```
485
+
486
+ ## Next Steps
487
+
488
+ Run `npx @majkapp/plugin-kit --context` - ctx API for onReady
489
+ Run `npx @majkapp/plugin-kit --testing` - Test plugin lifecycle
490
+ Run `npx @majkapp/plugin-kit --services` - Organize functions into services