@lydt/geogebra-studio 0.0.0-stage → 0.4.0-rc.2

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 (189) hide show
  1. package/.agents/plugins/marketplace.json +21 -0
  2. package/.codex-plugin/plugin.json +21 -0
  3. package/.mcp.json +18 -0
  4. package/LICENSE +339 -0
  5. package/LICENSE-MIT +21 -0
  6. package/README.md +108 -2
  7. package/README.zh-CN.md +89 -0
  8. package/THIRD_PARTY_NOTICES.md +35 -0
  9. package/dist/cli.d.ts +27 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +234 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/export/files.d.ts +4 -0
  14. package/dist/export/files.d.ts.map +1 -0
  15. package/dist/export/files.js +44 -0
  16. package/dist/export/files.js.map +1 -0
  17. package/dist/export/gikz.d.ts +11 -0
  18. package/dist/export/gikz.d.ts.map +1 -0
  19. package/dist/export/gikz.js +28 -0
  20. package/dist/export/gikz.js.map +1 -0
  21. package/dist/export/html.d.ts +14 -0
  22. package/dist/export/html.d.ts.map +1 -0
  23. package/dist/export/html.js +97 -0
  24. package/dist/export/html.js.map +1 -0
  25. package/dist/export/semantic.d.ts +43 -0
  26. package/dist/export/semantic.d.ts.map +1 -0
  27. package/dist/export/semantic.js +212 -0
  28. package/dist/export/semantic.js.map +1 -0
  29. package/dist/export/viewer-themes.d.ts +76 -0
  30. package/dist/export/viewer-themes.d.ts.map +1 -0
  31. package/dist/export/viewer-themes.js +169 -0
  32. package/dist/export/viewer-themes.js.map +1 -0
  33. package/dist/export/workspace.d.ts +4 -0
  34. package/dist/export/workspace.d.ts.map +1 -0
  35. package/dist/export/workspace.js +25 -0
  36. package/dist/export/workspace.js.map +1 -0
  37. package/dist/gikz-browser.js +770 -0
  38. package/dist/index.d.ts +5 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +84 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/server.d.ts +35 -0
  43. package/dist/server.d.ts.map +1 -0
  44. package/dist/server.js +187 -0
  45. package/dist/server.js.map +1 -0
  46. package/dist/studio/artifact.d.ts +37 -0
  47. package/dist/studio/artifact.d.ts.map +1 -0
  48. package/dist/studio/artifact.js +61 -0
  49. package/dist/studio/artifact.js.map +1 -0
  50. package/dist/studio/browser.d.ts +7 -0
  51. package/dist/studio/browser.d.ts.map +1 -0
  52. package/dist/studio/browser.js +67 -0
  53. package/dist/studio/browser.js.map +1 -0
  54. package/dist/studio/install.d.ts +4 -0
  55. package/dist/studio/install.d.ts.map +1 -0
  56. package/dist/studio/install.js +60 -0
  57. package/dist/studio/install.js.map +1 -0
  58. package/dist/tools/apps-api-tools.d.ts +4 -0
  59. package/dist/tools/apps-api-tools.d.ts.map +1 -0
  60. package/dist/tools/apps-api-tools.js +96 -0
  61. package/dist/tools/apps-api-tools.js.map +1 -0
  62. package/dist/tools/educational-templates.d.ts +33 -0
  63. package/dist/tools/educational-templates.d.ts.map +1 -0
  64. package/dist/tools/educational-templates.js +261 -0
  65. package/dist/tools/educational-templates.js.map +1 -0
  66. package/dist/tools/export-tools.d.ts +3 -0
  67. package/dist/tools/export-tools.d.ts.map +1 -0
  68. package/dist/tools/export-tools.js +118 -0
  69. package/dist/tools/export-tools.js.map +1 -0
  70. package/dist/tools/geogebra-tools.d.ts +15 -0
  71. package/dist/tools/geogebra-tools.d.ts.map +1 -0
  72. package/dist/tools/geogebra-tools.js +3442 -0
  73. package/dist/tools/geogebra-tools.js.map +1 -0
  74. package/dist/tools/index.d.ts +43 -0
  75. package/dist/tools/index.d.ts.map +1 -0
  76. package/dist/tools/index.js +183 -0
  77. package/dist/tools/index.js.map +1 -0
  78. package/dist/tools/performance-tools.d.ts +7 -0
  79. package/dist/tools/performance-tools.d.ts.map +1 -0
  80. package/dist/tools/performance-tools.js +312 -0
  81. package/dist/tools/performance-tools.js.map +1 -0
  82. package/dist/tools/templates/algebra-templates.d.ts +7 -0
  83. package/dist/tools/templates/algebra-templates.d.ts.map +1 -0
  84. package/dist/tools/templates/algebra-templates.js +568 -0
  85. package/dist/tools/templates/algebra-templates.js.map +1 -0
  86. package/dist/tools/templates/geometry-templates.d.ts +7 -0
  87. package/dist/tools/templates/geometry-templates.d.ts.map +1 -0
  88. package/dist/tools/templates/geometry-templates.js +492 -0
  89. package/dist/tools/templates/geometry-templates.js.map +1 -0
  90. package/dist/types/geogebra.d.ts +103 -0
  91. package/dist/types/geogebra.d.ts.map +1 -0
  92. package/dist/types/geogebra.js +34 -0
  93. package/dist/types/geogebra.js.map +1 -0
  94. package/dist/types/mcp.d.ts +125 -0
  95. package/dist/types/mcp.d.ts.map +1 -0
  96. package/dist/types/mcp.js +19 -0
  97. package/dist/types/mcp.js.map +1 -0
  98. package/dist/utils/app-info.d.ts +12 -0
  99. package/dist/utils/app-info.d.ts.map +1 -0
  100. package/dist/utils/app-info.js +30 -0
  101. package/dist/utils/app-info.js.map +1 -0
  102. package/dist/utils/errors.d.ts +37 -0
  103. package/dist/utils/errors.d.ts.map +1 -0
  104. package/dist/utils/errors.js +72 -0
  105. package/dist/utils/errors.js.map +1 -0
  106. package/dist/utils/gemini-compatibility.d.ts +39 -0
  107. package/dist/utils/gemini-compatibility.d.ts.map +1 -0
  108. package/dist/utils/gemini-compatibility.js +154 -0
  109. package/dist/utils/gemini-compatibility.js.map +1 -0
  110. package/dist/utils/geogebra-instance.d.ts +252 -0
  111. package/dist/utils/geogebra-instance.d.ts.map +1 -0
  112. package/dist/utils/geogebra-instance.js +1560 -0
  113. package/dist/utils/geogebra-instance.js.map +1 -0
  114. package/dist/utils/geogebra-mock.d.ts +172 -0
  115. package/dist/utils/geogebra-mock.d.ts.map +1 -0
  116. package/dist/utils/geogebra-mock.js +732 -0
  117. package/dist/utils/geogebra-mock.js.map +1 -0
  118. package/dist/utils/logger.d.ts +4 -0
  119. package/dist/utils/logger.d.ts.map +1 -0
  120. package/dist/utils/logger.js +46 -0
  121. package/dist/utils/logger.js.map +1 -0
  122. package/dist/utils/performance/index.d.ts +76 -0
  123. package/dist/utils/performance/index.d.ts.map +1 -0
  124. package/dist/utils/performance/index.js +163 -0
  125. package/dist/utils/performance/index.js.map +1 -0
  126. package/dist/utils/performance/instance-pool.d.ts +78 -0
  127. package/dist/utils/performance/instance-pool.d.ts.map +1 -0
  128. package/dist/utils/performance/instance-pool.js +268 -0
  129. package/dist/utils/performance/instance-pool.js.map +1 -0
  130. package/dist/utils/validation.d.ts +92 -0
  131. package/dist/utils/validation.d.ts.map +1 -0
  132. package/dist/utils/validation.js +557 -0
  133. package/dist/utils/validation.js.map +1 -0
  134. package/dist/widget.html +110 -0
  135. package/docs/APPS-API-BRIDGE.md +28 -0
  136. package/docs/MODERNIZATION.md +15 -0
  137. package/docs/TANGENT-REGRESSION.md +34 -0
  138. package/docs/VALIDATION.md +26 -0
  139. package/docs/VISUAL-STANDARD.md +26 -0
  140. package/docs/images/native-construction.png +0 -0
  141. package/examples/tangent-construction.json +26 -0
  142. package/gikz/LICENSE +339 -0
  143. package/gikz/PROVENANCE.md +8 -0
  144. package/gikz/browser.cjs +2 -0
  145. package/gikz/cli.cjs +195 -0
  146. package/gikz/core.cjs +806 -0
  147. package/mcp.json +20 -0
  148. package/package.json +107 -4
  149. package/plugin.json +35 -0
  150. package/runtime/package.json +4 -0
  151. package/scripts/build-plugin.cjs +15 -0
  152. package/scripts/build-widget.cjs +7 -0
  153. package/skills/geogebra-construct/SKILL.md +40 -0
  154. package/skills/geogebra-setup/SKILL.md +37 -0
  155. package/skills/geometry-export/SKILL.md +31 -0
  156. package/src/cli.ts +212 -0
  157. package/src/export/files.ts +26 -0
  158. package/src/export/gikz.ts +22 -0
  159. package/src/export/html.ts +108 -0
  160. package/src/export/semantic.ts +180 -0
  161. package/src/export/viewer-themes.ts +165 -0
  162. package/src/export/workspace.ts +15 -0
  163. package/src/index.ts +71 -0
  164. package/src/server.ts +168 -0
  165. package/src/studio/artifact.ts +50 -0
  166. package/src/studio/browser.ts +38 -0
  167. package/src/studio/install.ts +43 -0
  168. package/src/tools/apps-api-tools.ts +96 -0
  169. package/src/tools/educational-templates.ts +299 -0
  170. package/src/tools/export-tools.ts +110 -0
  171. package/src/tools/geogebra-tools.ts +3836 -0
  172. package/src/tools/index.ts +192 -0
  173. package/src/tools/performance-tools.ts +322 -0
  174. package/src/tools/templates/algebra-templates.ts +619 -0
  175. package/src/tools/templates/geometry-templates.ts +550 -0
  176. package/src/types/geogebra.ts +140 -0
  177. package/src/types/global.d.ts +31 -0
  178. package/src/types/mcp.ts +141 -0
  179. package/src/utils/app-info.ts +35 -0
  180. package/src/utils/errors.ts +97 -0
  181. package/src/utils/gemini-compatibility.ts +193 -0
  182. package/src/utils/geogebra-instance.ts +1712 -0
  183. package/src/utils/geogebra-mock.ts +867 -0
  184. package/src/utils/logger.ts +60 -0
  185. package/src/utils/performance/index.ts +211 -0
  186. package/src/utils/performance/instance-pool.ts +323 -0
  187. package/src/utils/validation.ts +647 -0
  188. package/tsconfig.json +35 -0
  189. package/widget/main.ts +12 -0
@@ -0,0 +1,60 @@
1
+ import winston from 'winston';
2
+
3
+ interface LogInfo {
4
+ timestamp: string;
5
+ level: string;
6
+ message: string;
7
+ stack?: string;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ // Check if we're in MCP mode (stdio communication)
12
+ // When piping input, process.stdin.isTTY is undefined, not false
13
+ const isMcpMode = !process.stdin.isTTY;
14
+
15
+ // Create logger instance
16
+ const logger = winston.createLogger({
17
+ level: isMcpMode ? 'error' : (process.env['LOG_LEVEL'] || 'info'),
18
+ format: winston.format.combine(
19
+ winston.format.timestamp({
20
+ format: 'YYYY-MM-DD HH:mm:ss'
21
+ }),
22
+ winston.format.errors({ stack: true }),
23
+ winston.format.printf((info) => {
24
+ const { timestamp, level, message, stack, ...meta } = info as unknown as LogInfo;
25
+ let log = `${timestamp} [${level.toUpperCase()}]: ${message}`;
26
+
27
+ if (Object.keys(meta).length > 0) {
28
+ log += ` ${JSON.stringify(meta)}`;
29
+ }
30
+
31
+ if (stack) {
32
+ log += `\n${stack}`;
33
+ }
34
+
35
+ return log;
36
+ })
37
+ ),
38
+ defaultMeta: { service: 'geogebra-mcp-server' },
39
+ transports: [
40
+ // Use stderr instead of stdout to avoid interfering with MCP JSON-RPC communication
41
+ new winston.transports.Console({
42
+ stderrLevels: ['error', 'warn', 'info', 'debug'],
43
+ format: winston.format.simple() // Remove colorization for MCP compatibility
44
+ })
45
+ ]
46
+ });
47
+
48
+ // Add file transport in production
49
+ if (process.env['NODE_ENV'] === 'production') {
50
+ logger.add(new winston.transports.File({
51
+ filename: 'logs/error.log',
52
+ level: 'error'
53
+ }));
54
+
55
+ logger.add(new winston.transports.File({
56
+ filename: 'logs/combined.log'
57
+ }));
58
+ }
59
+
60
+ export default logger;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Performance Monitoring and Optimization System
3
+ * GEB-9: Performance Optimization: Response Time and Resource Management
4
+ */
5
+
6
+ export interface PerformanceMetrics {
7
+ operationName: string;
8
+ startTime: number;
9
+ endTime: number;
10
+ duration: number;
11
+ success: boolean;
12
+ errorMessage?: string;
13
+ memoryUsage?: NodeJS.MemoryUsage;
14
+ metadata?: Record<string, any>;
15
+ }
16
+
17
+ export interface PerformanceThresholds {
18
+ [operationName: string]: {
19
+ warning: number; // milliseconds
20
+ critical: number; // milliseconds
21
+ };
22
+ }
23
+
24
+ export class PerformanceMonitor {
25
+ private static instance: PerformanceMonitor;
26
+ private metrics: PerformanceMetrics[] = [];
27
+ private thresholds: PerformanceThresholds;
28
+ private maxMetrics: number = 1000; // Keep last 1000 metrics
29
+
30
+ // PRD requirements: <2 second response time
31
+ private defaultThresholds: PerformanceThresholds = {
32
+ 'geogebra_eval_command': { warning: 1000, critical: 2000 },
33
+ 'geogebra_create_point': { warning: 500, critical: 1000 },
34
+ 'geogebra_create_line': { warning: 500, critical: 1000 },
35
+ 'geogebra_export_png': { warning: 1500, critical: 2000 },
36
+ 'geogebra_export_svg': { warning: 800, critical: 1500 },
37
+ 'geogebra_instance_init': { warning: 8000, critical: 15000 },
38
+ 'geogebra_clear_construction': { warning: 300, critical: 1000 },
39
+ 'default': { warning: 1000, critical: 2000 }
40
+ };
41
+
42
+ private constructor() {
43
+ this.thresholds = { ...this.defaultThresholds };
44
+ }
45
+
46
+ static getInstance(): PerformanceMonitor {
47
+ if (!PerformanceMonitor.instance) {
48
+ PerformanceMonitor.instance = new PerformanceMonitor();
49
+ }
50
+ return PerformanceMonitor.instance;
51
+ }
52
+
53
+ /**
54
+ * Start timing an operation
55
+ */
56
+ startTiming(operationName: string, metadata?: Record<string, any>): (success?: boolean, errorMessage?: string) => PerformanceMetrics {
57
+ const startTime = performance.now();
58
+ const startMemory = process.memoryUsage();
59
+
60
+ return (success: boolean = true, errorMessage?: string) => {
61
+ const endTime = performance.now();
62
+ const endMemory = process.memoryUsage();
63
+ const duration = endTime - startTime;
64
+
65
+ const metric: PerformanceMetrics = {
66
+ operationName,
67
+ startTime,
68
+ endTime,
69
+ duration,
70
+ success,
71
+ ...(errorMessage && { errorMessage }),
72
+ memoryUsage: {
73
+ rss: endMemory.rss - startMemory.rss,
74
+ heapTotal: endMemory.heapTotal - startMemory.heapTotal,
75
+ heapUsed: endMemory.heapUsed - startMemory.heapUsed,
76
+ external: endMemory.external - startMemory.external,
77
+ arrayBuffers: endMemory.arrayBuffers - startMemory.arrayBuffers
78
+ },
79
+ ...(metadata && { metadata })
80
+ };
81
+
82
+ this.addMetric(metric);
83
+ this.checkThresholds(metric);
84
+ return metric;
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Measure and record an async operation
90
+ */
91
+ async measureOperation<T>(
92
+ operationName: string,
93
+ operation: () => Promise<T>,
94
+ metadata?: Record<string, any>
95
+ ): Promise<T> {
96
+ const endTiming = this.startTiming(operationName, metadata);
97
+
98
+ try {
99
+ const result = await operation();
100
+ endTiming(true);
101
+ return result;
102
+ } catch (error) {
103
+ endTiming(false, error instanceof Error ? error.message : String(error));
104
+ throw error;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Add a metric to the collection
110
+ */
111
+ private addMetric(metric: PerformanceMetrics): void {
112
+ this.metrics.push(metric);
113
+
114
+ // Keep only the last maxMetrics entries
115
+ if (this.metrics.length > this.maxMetrics) {
116
+ this.metrics = this.metrics.slice(-this.maxMetrics);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Check if metric exceeds thresholds and log warnings
122
+ */
123
+ private checkThresholds(metric: PerformanceMetrics): void {
124
+ const threshold = this.thresholds[metric.operationName] || this.thresholds['default'];
125
+
126
+ if (threshold && metric.duration > threshold.critical) {
127
+ console.error(`🚨 CRITICAL: ${metric.operationName} took ${metric.duration.toFixed(2)}ms (threshold: ${threshold.critical}ms)`);
128
+ } else if (threshold && metric.duration > threshold.warning) {
129
+ console.warn(`⚠️ WARNING: ${metric.operationName} took ${metric.duration.toFixed(2)}ms (threshold: ${threshold.warning}ms)`);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Get performance statistics for an operation
135
+ */
136
+ getStats(operationName?: string): {
137
+ count: number;
138
+ averageDuration: number;
139
+ medianDuration: number;
140
+ minDuration: number;
141
+ maxDuration: number;
142
+ successRate: number;
143
+ p95Duration: number;
144
+ p99Duration: number;
145
+ } {
146
+ const relevantMetrics = operationName
147
+ ? this.metrics.filter(m => m.operationName === operationName)
148
+ : this.metrics;
149
+
150
+ if (relevantMetrics.length === 0) {
151
+ return {
152
+ count: 0,
153
+ averageDuration: 0,
154
+ medianDuration: 0,
155
+ minDuration: 0,
156
+ maxDuration: 0,
157
+ successRate: 0,
158
+ p95Duration: 0,
159
+ p99Duration: 0
160
+ };
161
+ }
162
+
163
+ const durations = relevantMetrics.map(m => m.duration).sort((a, b) => a - b);
164
+ const successCount = relevantMetrics.filter(m => m.success).length;
165
+
166
+ return {
167
+ count: relevantMetrics.length,
168
+ averageDuration: durations.reduce((sum, d) => sum + d, 0) / durations.length,
169
+ medianDuration:
170
+ durations.length % 2 === 0
171
+ ? ((durations[durations.length / 2 - 1] ?? 0) + (durations[durations.length / 2] ?? 0)) / 2
172
+ : durations[Math.floor(durations.length / 2)] ?? 0,
173
+ minDuration: durations[0] || 0,
174
+ maxDuration: durations[durations.length - 1] || 0,
175
+ successRate: successCount / relevantMetrics.length,
176
+ p95Duration: durations[Math.floor(durations.length * 0.95)] || 0,
177
+ p99Duration: durations[Math.floor(durations.length * 0.99)] || 0
178
+ };
179
+ }
180
+
181
+ /**
182
+ * Get all operation names with metrics
183
+ */
184
+ getOperationNames(): string[] {
185
+ return [...new Set(this.metrics.map(m => m.operationName))];
186
+ }
187
+
188
+ /**
189
+ * Clear all metrics
190
+ */
191
+ clearMetrics(): void {
192
+ this.metrics = [];
193
+ }
194
+
195
+ /**
196
+ * Export metrics for analysis
197
+ */
198
+ exportMetrics(): PerformanceMetrics[] {
199
+ return [...this.metrics];
200
+ }
201
+
202
+ /**
203
+ * Set custom thresholds
204
+ */
205
+ setThresholds(thresholds: Partial<PerformanceThresholds>): void {
206
+ Object.assign(this.thresholds, thresholds);
207
+ }
208
+ }
209
+
210
+ // Global instance
211
+ export const performanceMonitor = PerformanceMonitor.getInstance();
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Optimized GeoGebra Instance Pool Management
3
+ * GEB-9: Performance Optimization: Response Time and Resource Management
4
+ */
5
+
6
+ import { GeoGebraInstance } from '../geogebra-instance';
7
+ import { GeoGebraPoolConfig } from '../../types/geogebra';
8
+ import logger from '../logger';
9
+ import { performanceMonitor } from './index';
10
+
11
+ // Check if we're in MCP mode (stdio communication)
12
+ // When piping input, process.stdin.isTTY is undefined, not false
13
+ const isMcpMode = !process.stdin.isTTY;
14
+
15
+ export interface PooledInstance {
16
+ instance: GeoGebraInstance;
17
+ isActive: boolean;
18
+ lastUsed: Date;
19
+ createdAt: Date;
20
+ usageCount: number;
21
+ }
22
+
23
+ export class OptimizedInstancePool {
24
+ private static poolInstance: OptimizedInstancePool;
25
+ private instances: Map<string, PooledInstance> = new Map();
26
+ private config: GeoGebraPoolConfig;
27
+ private cleanupInterval?: NodeJS.Timeout | undefined;
28
+
29
+ private defaultConfig: GeoGebraPoolConfig = {
30
+ maxInstances: 3, // Maximum concurrent instances
31
+ instanceTimeout: 300000, // 5 minutes
32
+ maxIdleTime: 600000, // 10 minutes
33
+ headless: true,
34
+ browserArgs: [
35
+ '--no-sandbox',
36
+ '--disable-setuid-sandbox',
37
+ '--disable-dev-shm-usage',
38
+ '--disable-accelerated-2d-canvas',
39
+ '--no-first-run',
40
+ '--no-zygote',
41
+ '--disable-gpu',
42
+ '--memory-pressure-off', // Reduce memory pressure
43
+ '--max_old_space_size=512' // Limit memory usage
44
+ ]
45
+ };
46
+
47
+ private constructor(config?: Partial<GeoGebraPoolConfig>) {
48
+ this.config = { ...this.defaultConfig, ...config };
49
+ this.startCleanupTimer();
50
+
51
+ // Transport lifecycle owns graceful cleanup.
52
+
53
+ if (!isMcpMode) {
54
+ logger.info('Optimized Instance Pool initialized', { config: this.config });
55
+ }
56
+ }
57
+
58
+ static getInstance(config?: Partial<GeoGebraPoolConfig>): OptimizedInstancePool {
59
+ if (!OptimizedInstancePool.poolInstance) {
60
+ OptimizedInstancePool.poolInstance = new OptimizedInstancePool(config);
61
+ }
62
+ return OptimizedInstancePool.poolInstance;
63
+ }
64
+
65
+ /**
66
+ * Get an available instance from the pool
67
+ */
68
+ async getInstance(): Promise<GeoGebraInstance> {
69
+ return performanceMonitor.measureOperation('instance_pool_get', async () => {
70
+ // Try to find an available instance
71
+ let availableInstance = this.findAvailableInstance();
72
+
73
+ if (!availableInstance) {
74
+ // Create new instance if under limit
75
+ if (this.instances.size < this.config.maxInstances) {
76
+ availableInstance = await this.createNewInstance();
77
+ } else {
78
+ // Wait for an instance to become available or force cleanup
79
+ await this.forceCleanupOldest();
80
+ availableInstance = await this.createNewInstance();
81
+ }
82
+ }
83
+
84
+ // Mark as active and update usage stats
85
+ const pooled = this.instances.get(availableInstance.id)!;
86
+ pooled.isActive = true;
87
+ pooled.lastUsed = new Date();
88
+ pooled.usageCount++;
89
+
90
+ logger.debug(`Instance ${availableInstance.id} acquired from pool`, {
91
+ totalInstances: this.instances.size,
92
+ activeInstances: this.getActiveCount(),
93
+ usageCount: pooled.usageCount
94
+ });
95
+
96
+ return availableInstance;
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Release an instance back to the pool
102
+ */
103
+ async releaseInstance(instance: GeoGebraInstance): Promise<void> {
104
+ return performanceMonitor.measureOperation('instance_pool_release', async () => {
105
+ const pooled = this.instances.get(instance.id);
106
+ if (pooled) {
107
+ pooled.isActive = false;
108
+ pooled.lastUsed = new Date();
109
+
110
+ // Clear construction to reset state for next use
111
+ try {
112
+ await instance.newConstruction();
113
+ } catch (error) {
114
+ logger.warn(`Failed to clear construction on release: ${error}`);
115
+ }
116
+
117
+ logger.debug(`Instance ${instance.id} released to pool`, {
118
+ totalInstances: this.instances.size,
119
+ activeInstances: this.getActiveCount()
120
+ });
121
+ }
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Find an available (non-active) instance
127
+ */
128
+ private findAvailableInstance(): GeoGebraInstance | null {
129
+ for (const [, pooled] of this.instances) {
130
+ if (!pooled.isActive) {
131
+ return pooled.instance;
132
+ }
133
+ }
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * Create a new instance and add to pool
139
+ */
140
+ private async createNewInstance(): Promise<GeoGebraInstance> {
141
+ return performanceMonitor.measureOperation('instance_pool_create', async () => {
142
+ const instance = new GeoGebraInstance({
143
+ appName: 'classic',
144
+ width: 800,
145
+ height: 600,
146
+ showMenuBar: false,
147
+ showToolBar: false,
148
+ showAlgebraInput: false
149
+ });
150
+
151
+ await instance.initialize(this.config.headless, this.config.browserArgs);
152
+
153
+ const pooled: PooledInstance = {
154
+ instance,
155
+ isActive: false,
156
+ lastUsed: new Date(),
157
+ createdAt: new Date(),
158
+ usageCount: 0
159
+ };
160
+
161
+ this.instances.set(instance.id, pooled);
162
+
163
+ logger.info(`New instance ${instance.id} created and added to pool`, {
164
+ totalInstances: this.instances.size
165
+ });
166
+
167
+ return instance;
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Force cleanup of the oldest instance to make room
173
+ */
174
+ private async forceCleanupOldest(): Promise<void> {
175
+ let oldestId: string | null = null;
176
+ let oldestTime = Date.now();
177
+
178
+ // Find oldest inactive instance
179
+ for (const [id, pooled] of this.instances) {
180
+ if (!pooled.isActive && pooled.lastUsed.getTime() < oldestTime) {
181
+ oldestTime = pooled.lastUsed.getTime();
182
+ oldestId = id;
183
+ }
184
+ }
185
+
186
+ // If no inactive instance, find oldest active one
187
+ if (!oldestId) {
188
+ for (const [id, pooled] of this.instances) {
189
+ if (pooled.lastUsed.getTime() < oldestTime) {
190
+ oldestTime = pooled.lastUsed.getTime();
191
+ oldestId = id;
192
+ }
193
+ }
194
+ }
195
+
196
+ if (oldestId) {
197
+ await this.removeInstance(oldestId);
198
+ logger.info(`Forced cleanup of oldest instance ${oldestId} to make room`);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Remove and cleanup an instance
204
+ */
205
+ private async removeInstance(instanceId: string): Promise<void> {
206
+ const pooled = this.instances.get(instanceId);
207
+ if (pooled) {
208
+ try {
209
+ await pooled.instance.cleanup();
210
+ } catch (error) {
211
+ logger.error(`Error cleaning up instance ${instanceId}:`, error);
212
+ }
213
+ this.instances.delete(instanceId);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Start automatic cleanup timer
219
+ */
220
+ private startCleanupTimer(): void {
221
+ this.cleanupInterval = setInterval(() => {
222
+ this.performCleanup();
223
+ }, 60000); // Run cleanup every minute
224
+ this.cleanupInterval.unref();
225
+ }
226
+
227
+ /**
228
+ * Perform periodic cleanup of idle instances
229
+ */
230
+ private async performCleanup(): Promise<void> {
231
+ const now = Date.now();
232
+ const instancesToRemove: string[] = [];
233
+
234
+ for (const [id, pooled] of this.instances) {
235
+ const idleTime = now - pooled.lastUsed.getTime();
236
+ const isExpired = idleTime > this.config.maxIdleTime;
237
+ const isOld = (now - pooled.createdAt.getTime()) > this.config.instanceTimeout;
238
+
239
+ if (!pooled.isActive && (isExpired || isOld)) {
240
+ instancesToRemove.push(id);
241
+ }
242
+ }
243
+
244
+ for (const id of instancesToRemove) {
245
+ await this.removeInstance(id);
246
+ logger.debug(`Cleaned up idle instance ${id}`);
247
+ }
248
+
249
+ if (instancesToRemove.length > 0) {
250
+ logger.info(`Cleanup completed: removed ${instancesToRemove.length} idle instances`);
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Get count of active instances
256
+ */
257
+ private getActiveCount(): number {
258
+ return Array.from(this.instances.values()).filter(p => p.isActive).length;
259
+ }
260
+
261
+ /**
262
+ * Get pool statistics
263
+ */
264
+ getStats(): {
265
+ totalInstances: number;
266
+ activeInstances: number;
267
+ averageUsage: number;
268
+ oldestInstanceAge: number;
269
+ memoryEstimate: number; // MB
270
+ } {
271
+ const instances = Array.from(this.instances.values());
272
+ const now = Date.now();
273
+
274
+ return {
275
+ totalInstances: instances.length,
276
+ activeInstances: this.getActiveCount(),
277
+ averageUsage: instances.reduce((sum, p) => sum + p.usageCount, 0) / instances.length || 0,
278
+ oldestInstanceAge: instances.length > 0
279
+ ? Math.max(...instances.map(p => now - p.createdAt.getTime())) / 1000
280
+ : 0,
281
+ memoryEstimate: instances.length * 75 // Approximate 75MB per instance
282
+ };
283
+ }
284
+
285
+ /**
286
+ * Cleanup all instances and stop timers
287
+ */
288
+ async cleanup(): Promise<void> {
289
+ if (this.cleanupInterval) {
290
+ clearInterval(this.cleanupInterval);
291
+ this.cleanupInterval = undefined;
292
+ }
293
+
294
+ const cleanupPromises = Array.from(this.instances.keys()).map(id =>
295
+ this.removeInstance(id)
296
+ );
297
+
298
+ await Promise.all(cleanupPromises);
299
+
300
+ logger.info('Instance pool cleaned up completely');
301
+ }
302
+
303
+ /**
304
+ * Warm up the pool by pre-creating instances
305
+ */
306
+ async warmUp(count: number = 1): Promise<void> {
307
+ const warmUpPromises: Promise<void>[] = [];
308
+
309
+ for (let i = 0; i < count && this.instances.size < this.config.maxInstances; i++) {
310
+ warmUpPromises.push(
311
+ this.createNewInstance().then(instance =>
312
+ this.releaseInstance(instance)
313
+ )
314
+ );
315
+ }
316
+
317
+ await Promise.all(warmUpPromises);
318
+ logger.info(`Pool warmed up with ${count} pre-created instances`);
319
+ }
320
+ }
321
+
322
+ // Global optimized pool instance
323
+ export const optimizedInstancePool = OptimizedInstancePool.getInstance();