@vizzly-testing/cli 0.3.1 → 0.4.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.
Files changed (55) hide show
  1. package/README.md +26 -28
  2. package/dist/cli.js +18 -30
  3. package/dist/client/index.js +1 -1
  4. package/dist/commands/run.js +34 -9
  5. package/dist/commands/tdd.js +6 -1
  6. package/dist/commands/upload.js +52 -3
  7. package/dist/server/handlers/api-handler.js +83 -0
  8. package/dist/server/handlers/tdd-handler.js +138 -0
  9. package/dist/server/http-server.js +132 -0
  10. package/dist/services/api-service.js +40 -11
  11. package/dist/services/server-manager.js +45 -29
  12. package/dist/services/test-runner.js +64 -69
  13. package/dist/services/uploader.js +47 -82
  14. package/dist/types/commands/run.d.ts +4 -1
  15. package/dist/types/commands/tdd.d.ts +4 -1
  16. package/dist/types/sdk/index.d.ts +6 -6
  17. package/dist/types/server/handlers/api-handler.d.ts +49 -0
  18. package/dist/types/server/handlers/tdd-handler.d.ts +85 -0
  19. package/dist/types/server/http-server.d.ts +5 -0
  20. package/dist/types/services/api-service.d.ts +4 -2
  21. package/dist/types/services/server-manager.d.ts +148 -3
  22. package/dist/types/services/test-runner.d.ts +1 -0
  23. package/dist/types/utils/config-helpers.d.ts +1 -1
  24. package/dist/types/utils/console-ui.d.ts +1 -1
  25. package/dist/utils/console-ui.js +4 -14
  26. package/docs/api-reference.md +2 -5
  27. package/docs/getting-started.md +1 -1
  28. package/docs/tdd-mode.md +9 -9
  29. package/docs/test-integration.md +3 -17
  30. package/docs/upload-command.md +7 -0
  31. package/package.json +1 -1
  32. package/dist/screenshot-wrapper.js +0 -68
  33. package/dist/server/index.js +0 -522
  34. package/dist/services/service-utils.js +0 -171
  35. package/dist/types/index.js +0 -153
  36. package/dist/types/screenshot-wrapper.d.ts +0 -27
  37. package/dist/types/server/index.d.ts +0 -38
  38. package/dist/types/services/service-utils.d.ts +0 -45
  39. package/dist/types/types/index.d.ts +0 -373
  40. package/dist/types/utils/diagnostics.d.ts +0 -69
  41. package/dist/types/utils/error-messages.d.ts +0 -42
  42. package/dist/types/utils/framework-detector.d.ts +0 -5
  43. package/dist/types/utils/help.d.ts +0 -11
  44. package/dist/types/utils/image-comparison.d.ts +0 -42
  45. package/dist/types/utils/package.d.ts +0 -1
  46. package/dist/types/utils/project-detection.d.ts +0 -19
  47. package/dist/types/utils/ui-helpers.d.ts +0 -23
  48. package/dist/utils/diagnostics.js +0 -184
  49. package/dist/utils/error-messages.js +0 -34
  50. package/dist/utils/framework-detector.js +0 -40
  51. package/dist/utils/help.js +0 -66
  52. package/dist/utils/image-comparison.js +0 -172
  53. package/dist/utils/package.js +0 -9
  54. package/dist/utils/project-detection.js +0 -145
  55. package/dist/utils/ui-helpers.js +0 -86
@@ -1,522 +0,0 @@
1
- import { createServer } from 'http';
2
- import { randomUUID } from 'crypto';
3
- import { Buffer } from 'buffer';
4
- // import { createVizzly } from '../sdk/index.js'; // Commented out until needed
5
- import { createServiceLogger } from '../utils/logger-factory.js';
6
- import { TddService } from '../services/tdd-service.js';
7
- import { colors } from '../utils/colors.js';
8
- const logger = createServiceLogger('SERVER');
9
-
10
- // Constants for lazy build creation
11
- const VIZZLY_LAZY_BUILD_ID = 'lazy';
12
- export class VizzlyServer {
13
- constructor({
14
- port,
15
- config,
16
- tddMode = false,
17
- baselineBuild,
18
- baselineComparison,
19
- workingDir,
20
- buildId = null,
21
- vizzlyApi = null,
22
- buildInfo = null,
23
- emitter = null
24
- }) {
25
- this.port = port;
26
- this.config = config;
27
- this.builds = new Map();
28
- this.server = null;
29
- this.tddMode = tddMode;
30
- this.baselineBuild = baselineBuild;
31
- this.baselineComparison = baselineComparison;
32
- this.tddService = tddMode ? new TddService(config, workingDir) : null;
33
- this.buildId = buildId;
34
- this.vizzlyApi = vizzlyApi;
35
- this.buildInfo = buildInfo; // For lazy build creation
36
- this.emitter = emitter; // Event emitter for UI updates
37
- this.vizzlyDisabled = false; // Circuit breaker: disable Vizzly after first 500 error
38
- }
39
- async start() {
40
- // Initialize TDD mode if enabled
41
- if (this.tddMode && this.tddService) {
42
- logger.info('🔄 TDD mode enabled - setting up local comparison...');
43
-
44
- // Try to load existing baseline first
45
- const baseline = await this.tddService.loadBaseline();
46
- if (!baseline) {
47
- // Only try to download if we have an API token
48
- if (this.config.apiKey) {
49
- logger.info('📥 No local baseline found, downloading from Vizzly...');
50
- // Download baseline from the latest passed build
51
- await this.tddService.downloadBaselines(this.baselineBuild, this.baselineComparison);
52
- } else {
53
- logger.info('📝 No local baseline found and no API token - all screenshots will be marked as new');
54
- }
55
- } else {
56
- logger.info(`✅ Using existing baseline: ${colors.cyan(baseline.buildName)}`);
57
- }
58
- }
59
-
60
- // Register active build if provided
61
- if (this.buildId) {
62
- this.builds.set(this.buildId, {
63
- id: this.buildId,
64
- name: `Active Build ${this.buildId}`,
65
- branch: 'current',
66
- environment: 'test',
67
- screenshots: [],
68
- createdAt: Date.now()
69
- });
70
- logger.debug(`Registered active build: ${this.buildId}`);
71
- }
72
- return new Promise((resolve, reject) => {
73
- this.server = createServer(async (req, res) => {
74
- try {
75
- await this.handleRequest(req, res);
76
- } catch (error) {
77
- logger.error('Server error:', error);
78
- res.statusCode = 500;
79
- res.setHeader('Content-Type', 'application/json');
80
- res.end(JSON.stringify({
81
- error: 'Internal server error'
82
- }));
83
- }
84
- });
85
- this.server.listen(this.port, '127.0.0.1', error => {
86
- if (error) {
87
- reject(error);
88
- } else {
89
- logger.debug(`HTTP server listening on http://127.0.0.1:${this.port}`);
90
- resolve();
91
- }
92
- });
93
- this.server.on('error', error => {
94
- if (error.code === 'EADDRINUSE') {
95
- reject(new Error(`Port ${this.port} is already in use. Try a different port with --port.`));
96
- } else {
97
- reject(error);
98
- }
99
- });
100
- });
101
- }
102
- async handleRequest(req, res) {
103
- // Set CORS headers
104
- res.setHeader('Access-Control-Allow-Origin', '*');
105
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
106
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
107
- res.setHeader('Content-Type', 'application/json');
108
- if (req.method === 'OPTIONS') {
109
- res.statusCode = 200;
110
- res.end();
111
- return;
112
- }
113
- if (req.method === 'GET' && req.url === '/health') {
114
- res.statusCode = 200;
115
- res.end(JSON.stringify({
116
- status: 'ok',
117
- builds: this.builds.size,
118
- port: this.port,
119
- uptime: process.uptime()
120
- }));
121
- return;
122
- }
123
- const screenshotPath = this.config?.server?.screenshotPath || '/screenshot';
124
- if (req.method === 'POST' && req.url === screenshotPath) {
125
- await this.handleScreenshot(req, res);
126
- return;
127
- }
128
- res.statusCode = 404;
129
- res.end(JSON.stringify({
130
- error: 'Not found'
131
- }));
132
- }
133
- async handleScreenshot(req, res) {
134
- try {
135
- const body = await this.parseRequestBody(req);
136
- let {
137
- buildId,
138
- name,
139
- properties,
140
- image
141
- } = body;
142
- if (!buildId || !name || !image) {
143
- res.statusCode = 400;
144
- res.end(JSON.stringify({
145
- error: 'buildId, name, and image are required'
146
- }));
147
- return;
148
- }
149
-
150
- // If Vizzly has been disabled due to server errors, skip upload and continue tests
151
- if (this.vizzlyDisabled) {
152
- logger.debug(`Screenshot captured (Vizzly disabled): ${name}`);
153
-
154
- // Create a mock build entry to track screenshot count for user feedback
155
- const mockBuildId = 'disabled-build';
156
- if (!this.builds.has(mockBuildId)) {
157
- this.builds.set(mockBuildId, {
158
- id: mockBuildId,
159
- name: 'Disabled Build',
160
- screenshots: [],
161
- createdAt: Date.now()
162
- });
163
- }
164
- const mockBuild = this.builds.get(mockBuildId);
165
- mockBuild.screenshots.push({
166
- name,
167
- timestamp: Date.now(),
168
- disabled: true
169
- });
170
- res.statusCode = 200;
171
- res.end(JSON.stringify({
172
- success: true,
173
- disabled: true,
174
- count: mockBuild.screenshots.length,
175
- message: `Vizzly disabled - ${mockBuild.screenshots.length} screenshots captured but not uploaded`
176
- }));
177
- return;
178
- }
179
-
180
- // Handle lazy build creation or mapping
181
- if (buildId === VIZZLY_LAZY_BUILD_ID) {
182
- if (this.buildId) {
183
- // Build already created, use existing build ID
184
- buildId = this.buildId;
185
- } else if (this.buildInfo && this.vizzlyApi) {
186
- // Create build now
187
- const creatingMessage = '🏗️ Creating build (first screenshot captured)...';
188
- logger.debug(creatingMessage); // Change to debug level
189
- // Don't emit log event - let the build-created event handle UI updates
190
-
191
- try {
192
- const buildResult = await this.vizzlyApi.createBuild({
193
- build: {
194
- name: this.buildInfo.buildName,
195
- branch: this.buildInfo.branch,
196
- environment: this.buildInfo.environment || 'test',
197
- commit_sha: this.buildInfo.commitSha,
198
- commit_message: this.buildInfo.commitMessage
199
- }
200
- });
201
- this.buildId = buildResult.id;
202
- buildId = this.buildId; // Update local variable
203
- const buildUrl = buildResult.url;
204
-
205
- // Register the build in our local map
206
- this.builds.set(this.buildId, {
207
- id: this.buildId,
208
- name: this.buildInfo.buildName,
209
- branch: this.buildInfo.branch,
210
- environment: 'test',
211
- screenshots: [],
212
- createdAt: Date.now()
213
- });
214
- const createdMessage = `✅ Build created: ${this.buildInfo.buildName}`;
215
- const urlMessage = `🔗 Build URL: ${buildUrl}`;
216
- logger.debug(createdMessage); // Change to debug level
217
- logger.debug(urlMessage); // Change to debug level
218
- // Don't emit log events - the build-created event will handle UI
219
- if (this.emitter) {
220
- this.emitter.emit('build-created', {
221
- buildId: this.buildId,
222
- url: buildUrl,
223
- name: this.buildInfo.buildName
224
- });
225
- }
226
-
227
- // Clear buildInfo since we no longer need it
228
- this.buildInfo = null;
229
- } catch (buildError) {
230
- logger.error('Failed to create build:', {
231
- error: buildError.message,
232
- code: buildError.code,
233
- stack: buildError.stack,
234
- buildInfo: this.buildInfo,
235
- apiUrl: this.vizzlyApi?.apiUrl,
236
- hasApiKey: !!this.config.apiKey
237
- });
238
-
239
- // Log additional context for debugging
240
- if (buildError.response) {
241
- logger.error('API Response details:', {
242
- status: buildError.response.status,
243
- statusText: buildError.response.statusText,
244
- headers: buildError.response.headers
245
- });
246
- }
247
-
248
- // Disable Vizzly on any build creation error
249
- this.vizzlyDisabled = true;
250
- const disabledMessage = `⚠️ Vizzly disabled due to build creation error: ${buildError.message} - continuing tests without visual testing`;
251
- logger.warn(disabledMessage);
252
- if (this.emitter) this.emitter.emit('log', disabledMessage);
253
-
254
- // Return success to allow tests to continue
255
- res.statusCode = 200;
256
- res.end(JSON.stringify({
257
- success: true,
258
- disabled: true,
259
- message: 'Vizzly disabled due to build creation error - screenshot captured but not uploaded'
260
- }));
261
- return;
262
- }
263
- } else {
264
- // No buildInfo available and no existing build - this shouldn't happen in lazy mode
265
- res.statusCode = 400;
266
- res.end(JSON.stringify({
267
- error: 'Build creation failed - lazy mode requires valid configuration'
268
- }));
269
- return;
270
- }
271
- }
272
- const build = this.builds.get(buildId);
273
- if (!build) {
274
- res.statusCode = 404;
275
- res.end(JSON.stringify({
276
- error: 'Build not found'
277
- }));
278
- return;
279
- }
280
-
281
- // Store screenshot data (image is base64 encoded)
282
- const screenshot = {
283
- name,
284
- imageData: image,
285
- properties: properties || {},
286
- timestamp: Date.now()
287
- };
288
- build.screenshots.push(screenshot);
289
-
290
- // Log screenshot capture (debug only, don't spam UI)
291
- logger.debug(`Screenshot captured: ${name}`);
292
-
293
- // Emit count update instead of individual logs
294
- if (this.emitter) {
295
- this.emitter.emit('screenshot-captured', {
296
- name,
297
- count: build.screenshots.length
298
- });
299
- }
300
-
301
- // Handle TDD mode comparison - fail fast on visual differences
302
- if (this.tddMode && this.tddService) {
303
- const imageBuffer = Buffer.from(image, 'base64');
304
- const comparison = await this.tddService.compareScreenshot(name, imageBuffer, properties || {});
305
- if (comparison.status === 'failed') {
306
- // Visual difference detected - fail immediately (clean logging for TDD)
307
- res.statusCode = 422; // Unprocessable Entity
308
- res.end(JSON.stringify({
309
- error: 'Visual difference detected',
310
- details: `Screenshot '${name}' differs from baseline`,
311
- comparison: {
312
- name: comparison.name,
313
- status: comparison.status,
314
- baseline: comparison.baseline,
315
- current: comparison.current,
316
- diff: comparison.diff
317
- },
318
- tddMode: true
319
- }));
320
- return;
321
- }
322
- if (comparison.status === 'baseline-updated') {
323
- // Baseline was updated successfully
324
- res.statusCode = 200;
325
- res.end(JSON.stringify({
326
- status: 'success',
327
- message: `Baseline updated for ${name}`,
328
- comparison: {
329
- name: comparison.name,
330
- status: comparison.status,
331
- baseline: comparison.baseline,
332
- current: comparison.current
333
- },
334
- tddMode: true
335
- }));
336
- return;
337
- }
338
- if (comparison.status === 'error') {
339
- // Comparison error (clean logging for TDD)
340
- res.statusCode = 500;
341
- res.end(JSON.stringify({
342
- error: `Comparison failed: ${comparison.error}`,
343
- tddMode: true
344
- }));
345
- return;
346
- }
347
-
348
- // Success (passed or new)
349
- logger.debug(`✅ TDD: ${comparison.status.toUpperCase()} ${name}`);
350
- res.statusCode = 200;
351
- res.end(JSON.stringify({
352
- success: true,
353
- comparison: {
354
- name: comparison.name,
355
- status: comparison.status
356
- },
357
- tddMode: true
358
- }));
359
- return;
360
- }
361
-
362
- // Non-TDD mode: Upload screenshot immediately to API
363
- if (this.vizzlyApi && buildId !== VIZZLY_LAZY_BUILD_ID && !this.vizzlyDisabled) {
364
- try {
365
- const imageBuffer = Buffer.from(image, 'base64');
366
- const result = await this.vizzlyApi.uploadScreenshot(buildId, name, imageBuffer, properties ?? {});
367
-
368
- // Log upload or skip
369
- if (result.skipped) {
370
- logger.debug(`Screenshot already exists, skipped: ${name}`);
371
- } else {
372
- logger.debug(`Screenshot uploaded: ${name}`);
373
- }
374
- if (this.emitter) this.emitter.emit('screenshot-uploaded', {
375
- name,
376
- skipped: result.skipped
377
- });
378
- } catch (uploadError) {
379
- logger.error(`❌ Failed to upload screenshot ${name}:`, uploadError.message);
380
-
381
- // Disable Vizzly on any upload error
382
- this.vizzlyDisabled = true;
383
- const disabledMessage = '⚠️ Vizzly disabled due to upload error - continuing tests without visual testing';
384
- logger.warn(disabledMessage);
385
- if (this.emitter) this.emitter.emit('log', disabledMessage);
386
- // Continue anyway - don't fail the test for upload errors
387
- }
388
- }
389
- logger.debug(`Screenshot received: ${name}`);
390
- res.statusCode = 200;
391
- res.end(JSON.stringify({
392
- success: true,
393
- count: build.screenshots.length,
394
- name: screenshot.name,
395
- tddMode: false
396
- }));
397
- } catch (error) {
398
- logger.error('Screenshot upload error:', error);
399
- res.statusCode = 500;
400
- res.end(JSON.stringify({
401
- error: 'Failed to process screenshot'
402
- }));
403
- }
404
- }
405
- async parseRequestBody(req) {
406
- return new Promise((resolve, reject) => {
407
- let body = '';
408
- req.on('data', chunk => {
409
- body += chunk.toString();
410
- });
411
- req.on('end', () => {
412
- try {
413
- const data = JSON.parse(body);
414
- resolve(data);
415
- } catch {
416
- reject(new Error('Invalid JSON'));
417
- }
418
- });
419
- req.on('error', reject);
420
- });
421
- }
422
- async stop() {
423
- if (this.server) {
424
- return new Promise(resolve => {
425
- this.server.close(() => {
426
- this.server = null;
427
- logger.debug('HTTP server stopped');
428
- resolve();
429
- });
430
- });
431
- }
432
-
433
- // Clear builds from memory
434
- this.builds.clear();
435
- logger.debug('Cleanup completed');
436
- }
437
- async createBuild(options) {
438
- const buildId = randomUUID();
439
- const build = {
440
- id: buildId,
441
- name: options.name,
442
- branch: options.branch,
443
- environment: options.environment,
444
- screenshots: [],
445
- createdAt: Date.now()
446
- };
447
- this.builds.set(buildId, build);
448
- logger.debug(`Build created: ${buildId} - ${options.name}`);
449
- return buildId;
450
- }
451
- getScreenshotCount(buildId) {
452
- const build = this.builds.get(buildId);
453
- return build ? build.screenshots.length : 0;
454
- }
455
- getTotalScreenshotCount() {
456
- let total = 0;
457
- for (const build of this.builds.values()) {
458
- total += build.screenshots.length;
459
- }
460
- return total;
461
- }
462
- async finishBuild(buildId) {
463
- const build = this.builds.get(buildId);
464
- if (!build) {
465
- throw new Error(`Build ${buildId} not found`);
466
- }
467
- if (build.screenshots.length === 0) {
468
- throw new Error('No screenshots to upload. Make sure your tests are calling the Vizzly screenshot function.');
469
- }
470
-
471
- // Handle TDD mode completion
472
- if (this.tddMode && this.tddService) {
473
- const results = this.tddService.printResults();
474
-
475
- // Cleanup this build
476
- await this.cleanupBuild(buildId);
477
-
478
- // Return TDD results instead of uploading
479
- return {
480
- id: buildId,
481
- name: build.name,
482
- tddMode: true,
483
- results,
484
- url: null,
485
- // No URL for TDD mode
486
- passed: results.failed === 0 && results.errors === 0
487
- };
488
- }
489
-
490
- // Upload to Vizzly API using existing SDK
491
- const vizzly = createVizzly(this.config);
492
- await vizzly.startBuild({
493
- name: build.name,
494
- branch: build.branch,
495
- environment: build.environment
496
- });
497
-
498
- // Upload each screenshot
499
- for (const screenshot of build.screenshots) {
500
- const imageBuffer = Buffer.from(screenshot.imageData, 'base64');
501
- await vizzly.screenshot({
502
- name: screenshot.name,
503
- image: imageBuffer,
504
- properties: screenshot.properties
505
- });
506
- }
507
- const result = await vizzly.finishBuild();
508
-
509
- // Cleanup this build
510
- await this.cleanupBuild(buildId);
511
- logger.debug(`Build ${buildId} uploaded successfully as ${result.id}`);
512
- return result;
513
- }
514
- async cleanupBuild(buildId) {
515
- const build = this.builds.get(buildId);
516
- if (!build) return;
517
-
518
- // Remove from memory
519
- this.builds.delete(buildId);
520
- logger.debug(`Build ${buildId} cleaned up`);
521
- }
522
- }
@@ -1,171 +0,0 @@
1
- /**
2
- * Service Utilities
3
- *
4
- * Provides utilities for service composition using higher-order functions
5
- * and event-based architecture patterns.
6
- */
7
-
8
- import { EventEmitter } from 'events';
9
-
10
- /**
11
- * Create an event emitter with enhanced functionality
12
- * @returns {EventEmitter} Enhanced event emitter
13
- */
14
- export function createEventEmitter() {
15
- const emitter = new EventEmitter();
16
-
17
- // Add helper methods
18
- emitter.emitProgress = (stage, message, data = {}) => {
19
- const progressData = {
20
- stage,
21
- message,
22
- timestamp: new Date().toISOString(),
23
- ...data
24
- };
25
- emitter.emit('progress', progressData);
26
- };
27
- emitter.emitError = (error, context = {}) => {
28
- emitter.emit('error', {
29
- error: error.message,
30
- stack: error.stack,
31
- context,
32
- timestamp: new Date().toISOString()
33
- });
34
- };
35
- return emitter;
36
- }
37
-
38
- /**
39
- * Create a cleanup manager
40
- * @returns {Object} Cleanup manager with add/execute methods
41
- */
42
- export function createCleanupManager() {
43
- const cleanupFunctions = [];
44
- return {
45
- add: fn => cleanupFunctions.push(fn),
46
- execute: async () => {
47
- for (const fn of cleanupFunctions) {
48
- try {
49
- await fn();
50
- } catch (error) {
51
- console.error('Cleanup error:', error);
52
- }
53
- }
54
- cleanupFunctions.length = 0;
55
- }
56
- };
57
- }
58
-
59
- /**
60
- * Create signal handlers for graceful shutdown
61
- * @param {Function} onSignal - Function to call on signal
62
- * @returns {Function} Cleanup function to remove handlers
63
- */
64
- export function createSignalHandlers(onSignal) {
65
- const handleSignal = async signal => {
66
- await onSignal(signal);
67
- process.exit(signal === 'SIGINT' ? 130 : 1);
68
- };
69
- process.once('SIGINT', () => handleSignal('SIGINT'));
70
- process.once('SIGTERM', () => handleSignal('SIGTERM'));
71
- return () => {
72
- process.removeAllListeners('SIGINT');
73
- process.removeAllListeners('SIGTERM');
74
- };
75
- }
76
-
77
- /**
78
- * Higher-order function to add error handling to any function
79
- * @param {Function} fn - Function to wrap
80
- * @param {Object} options - Error handling options
81
- * @returns {Function} Wrapped function with error handling
82
- */
83
- export function withErrorHandling(fn, options = {}) {
84
- return async (...args) => {
85
- try {
86
- return await fn(...args);
87
- } catch (error) {
88
- if (options.onError) {
89
- options.onError(error);
90
- }
91
- if (options.rethrow !== false) {
92
- throw error;
93
- }
94
- return options.defaultReturn;
95
- }
96
- };
97
- }
98
-
99
- /**
100
- * Higher-order function to add logging to any function
101
- * @param {Function} fn - Function to wrap
102
- * @param {Object} logger - Logger instance
103
- * @param {string} operation - Operation name for logging
104
- * @returns {Function} Wrapped function with logging
105
- */
106
- export function withLogging(fn, logger, operation) {
107
- return async (...args) => {
108
- logger.debug(`Starting ${operation}`);
109
- const start = Date.now();
110
- try {
111
- const result = await fn(...args);
112
- const duration = Date.now() - start;
113
- logger.debug(`Completed ${operation} in ${duration}ms`);
114
- return result;
115
- } catch (error) {
116
- const duration = Date.now() - start;
117
- logger.error(`Failed ${operation} after ${duration}ms:`, error.message);
118
- throw error;
119
- }
120
- };
121
- }
122
-
123
- /**
124
- * Compose multiple functions together
125
- * @param {...Function} fns - Functions to compose
126
- * @returns {Function} Composed function
127
- */
128
- export function compose(...fns) {
129
- return value => fns.reduceRight((acc, fn) => fn(acc), value);
130
- }
131
-
132
- /**
133
- * Create a service context with shared functionality
134
- * @param {Object} config - Service configuration
135
- * @param {Object} options - Service options
136
- * @returns {Object} Service context
137
- */
138
- export function createServiceContext(config, options = {}) {
139
- const emitter = createEventEmitter(options);
140
- const cleanup = createCleanupManager();
141
- let isRunning = false;
142
- const context = {
143
- config,
144
- emitter,
145
- cleanup,
146
- get isRunning() {
147
- return isRunning;
148
- },
149
- set isRunning(value) {
150
- isRunning = value;
151
- },
152
- // Convenience methods
153
- emitProgress: emitter.emitProgress,
154
- emitError: emitter.emitError,
155
- emit: emitter.emit.bind(emitter),
156
- on: emitter.on.bind(emitter),
157
- off: emitter.off.bind(emitter),
158
- once: emitter.once.bind(emitter),
159
- // Signal handling
160
- setupSignalHandlers: () => {
161
- const removeHandlers = createSignalHandlers(async signal => {
162
- if (isRunning) {
163
- emitter.emitProgress('cleanup', `Received ${signal}, cleaning up...`);
164
- await cleanup.execute();
165
- }
166
- });
167
- cleanup.add(removeHandlers);
168
- }
169
- };
170
- return context;
171
- }