cache-overflow-mcp 0.3.3 → 0.3.5
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/.env.example +3 -3
- package/AGENTS.md +24 -3
- package/README.md +59 -0
- package/TROUBLESHOOTING.md +219 -0
- package/dist/cli.js +13 -1
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +54 -10
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/logger.d.ts +16 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +127 -0
- package/dist/logger.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +50 -7
- package/dist/server.js.map +1 -1
- package/dist/testing/mock-data.js +40 -40
- package/dist/ui/verification-dialog.d.ts.map +1 -1
- package/dist/ui/verification-dialog.js +307 -268
- package/dist/ui/verification-dialog.js.map +1 -1
- package/package.json +3 -2
- package/scripts/view-logs.js +125 -0
- package/src/cli.ts +23 -10
- package/src/client.test.ts +116 -116
- package/src/client.ts +122 -76
- package/src/config.ts +14 -9
- package/src/index.ts +3 -3
- package/src/logger.ts +150 -0
- package/src/server.ts +49 -7
- package/src/testing/mock-data.ts +142 -142
- package/src/testing/mock-server.ts +176 -176
- package/src/tools/index.ts +23 -23
- package/src/types.ts +39 -39
- package/src/ui/verification-dialog.ts +382 -342
- package/tsconfig.json +20 -20
- package/dist/tools/get-balance.d.ts +0 -3
- package/dist/tools/get-balance.d.ts.map +0 -1
- package/dist/tools/get-balance.js +0 -34
- package/dist/tools/get-balance.js.map +0 -1
package/src/logger.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
|
|
5
|
+
interface LogEntry {
|
|
6
|
+
timestamp: string;
|
|
7
|
+
level: 'ERROR' | 'WARN' | 'INFO';
|
|
8
|
+
message: string;
|
|
9
|
+
error?: {
|
|
10
|
+
name: string;
|
|
11
|
+
message: string;
|
|
12
|
+
stack?: string;
|
|
13
|
+
};
|
|
14
|
+
context?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class Logger {
|
|
18
|
+
private logFilePath: string;
|
|
19
|
+
private maxLogSizeBytes = 5 * 1024 * 1024; // 5MB max log file size
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
// Determine log file location
|
|
23
|
+
const logDir = process.env.CACHE_OVERFLOW_LOG_DIR ?? path.join(os.homedir(), '.cache-overflow');
|
|
24
|
+
|
|
25
|
+
// Ensure log directory exists
|
|
26
|
+
if (!fs.existsSync(logDir)) {
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// Fallback to temp directory if home directory is not writable
|
|
31
|
+
const tempLogDir = path.join(os.tmpdir(), 'cache-overflow');
|
|
32
|
+
if (!fs.existsSync(tempLogDir)) {
|
|
33
|
+
fs.mkdirSync(tempLogDir, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
this.logFilePath = path.join(tempLogDir, 'cache-overflow-mcp.log');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.logFilePath = path.join(logDir, 'cache-overflow-mcp.log');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private rotateLogIfNeeded(): void {
|
|
44
|
+
try {
|
|
45
|
+
if (fs.existsSync(this.logFilePath)) {
|
|
46
|
+
const stats = fs.statSync(this.logFilePath);
|
|
47
|
+
if (stats.size >= this.maxLogSizeBytes) {
|
|
48
|
+
// Rotate: keep the last 1MB of the file
|
|
49
|
+
const content = fs.readFileSync(this.logFilePath, 'utf-8');
|
|
50
|
+
const lines = content.split('\n');
|
|
51
|
+
const keptLines = lines.slice(-1000); // Keep last 1000 lines
|
|
52
|
+
fs.writeFileSync(this.logFilePath, keptLines.join('\n') + '\n');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// Ignore rotation errors - don't want logging to break the app
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private sanitizeContext(context?: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
61
|
+
if (!context) return undefined;
|
|
62
|
+
|
|
63
|
+
const sanitized: Record<string, unknown> = {};
|
|
64
|
+
for (const [key, value] of Object.entries(context)) {
|
|
65
|
+
// Sanitize sensitive fields
|
|
66
|
+
if (key.toLowerCase().includes('token') ||
|
|
67
|
+
key.toLowerCase().includes('password') ||
|
|
68
|
+
key.toLowerCase().includes('secret') ||
|
|
69
|
+
key.toLowerCase().includes('auth')) {
|
|
70
|
+
sanitized[key] = '[REDACTED]';
|
|
71
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
72
|
+
sanitized[key] = this.sanitizeContext(value as Record<string, unknown>);
|
|
73
|
+
} else {
|
|
74
|
+
sanitized[key] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return sanitized;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private writeLog(entry: LogEntry): void {
|
|
81
|
+
try {
|
|
82
|
+
this.rotateLogIfNeeded();
|
|
83
|
+
const logLine = JSON.stringify(entry) + '\n';
|
|
84
|
+
fs.appendFileSync(this.logFilePath, logLine);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// If we can't write to log file, write to stderr as fallback
|
|
87
|
+
console.error('Failed to write to log file:', err);
|
|
88
|
+
console.error('Original log entry:', entry);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
error(message: string, error?: Error, context?: Record<string, unknown>): void {
|
|
93
|
+
const entry: LogEntry = {
|
|
94
|
+
timestamp: new Date().toISOString(),
|
|
95
|
+
level: 'ERROR',
|
|
96
|
+
message,
|
|
97
|
+
context: this.sanitizeContext(context),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (error) {
|
|
101
|
+
entry.error = {
|
|
102
|
+
name: error.name,
|
|
103
|
+
message: error.message,
|
|
104
|
+
stack: error.stack,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
this.writeLog(entry);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
warn(message: string, context?: Record<string, unknown>): void {
|
|
112
|
+
const entry: LogEntry = {
|
|
113
|
+
timestamp: new Date().toISOString(),
|
|
114
|
+
level: 'WARN',
|
|
115
|
+
message,
|
|
116
|
+
context: this.sanitizeContext(context),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
this.writeLog(entry);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
info(message: string, context?: Record<string, unknown>): void {
|
|
123
|
+
const entry: LogEntry = {
|
|
124
|
+
timestamp: new Date().toISOString(),
|
|
125
|
+
level: 'INFO',
|
|
126
|
+
message,
|
|
127
|
+
context: this.sanitizeContext(context),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
this.writeLog(entry);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getLogFilePath(): string {
|
|
134
|
+
return this.logFilePath;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
logStartup(): void {
|
|
138
|
+
this.info('MCP Server starting', {
|
|
139
|
+
version: '0.3.5',
|
|
140
|
+
nodeVersion: process.version,
|
|
141
|
+
platform: process.platform,
|
|
142
|
+
arch: process.arch,
|
|
143
|
+
apiUrl: process.env.CACHE_OVERFLOW_URL ?? 'https://cache-overflow.onrender.com/api',
|
|
144
|
+
hasAuthToken: !!process.env.CACHE_OVERFLOW_TOKEN,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Singleton instance
|
|
150
|
+
export const logger = new Logger();
|
package/src/server.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
import { CacheOverflowClient } from './client.js';
|
|
10
10
|
import { tools } from './tools/index.js';
|
|
11
11
|
import { prompts } from './prompts/index.js';
|
|
12
|
+
import { logger } from './logger.js';
|
|
12
13
|
|
|
13
14
|
export class CacheOverflowServer {
|
|
14
15
|
private server: Server;
|
|
@@ -18,7 +19,7 @@ export class CacheOverflowServer {
|
|
|
18
19
|
this.server = new Server(
|
|
19
20
|
{
|
|
20
21
|
name: 'cache-overflow',
|
|
21
|
-
version: '0.3.
|
|
22
|
+
version: '0.3.5',
|
|
22
23
|
},
|
|
23
24
|
{
|
|
24
25
|
capabilities: {
|
|
@@ -41,9 +42,28 @@ export class CacheOverflowServer {
|
|
|
41
42
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
42
43
|
const tool = tools.find((t) => t.definition.name === request.params.name);
|
|
43
44
|
if (!tool) {
|
|
44
|
-
|
|
45
|
+
const error = new Error(`Unknown tool: ${request.params.name}`);
|
|
46
|
+
logger.error('Unknown tool requested', error, {
|
|
47
|
+
toolName: request.params.name,
|
|
48
|
+
availableTools: tools.map(t => t.definition.name),
|
|
49
|
+
});
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
logger.info(`Executing tool: ${request.params.name}`, {
|
|
55
|
+
toolName: request.params.name,
|
|
56
|
+
// Don't log full arguments as they might contain sensitive data
|
|
57
|
+
hasArguments: Object.keys(request.params.arguments ?? {}).length > 0,
|
|
58
|
+
});
|
|
59
|
+
return await tool.handler(request.params.arguments ?? {}, this.client);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
logger.error(`Tool execution failed: ${request.params.name}`, error as Error, {
|
|
62
|
+
toolName: request.params.name,
|
|
63
|
+
errorType: 'TOOL_EXECUTION_FAILURE',
|
|
64
|
+
});
|
|
65
|
+
throw error;
|
|
45
66
|
}
|
|
46
|
-
return tool.handler(request.params.arguments ?? {}, this.client);
|
|
47
67
|
});
|
|
48
68
|
|
|
49
69
|
// Prompt handlers
|
|
@@ -54,14 +74,36 @@ export class CacheOverflowServer {
|
|
|
54
74
|
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
55
75
|
const prompt = prompts.find((p) => p.definition.name === request.params.name);
|
|
56
76
|
if (!prompt) {
|
|
57
|
-
|
|
77
|
+
const error = new Error(`Unknown prompt: ${request.params.name}`);
|
|
78
|
+
logger.error('Unknown prompt requested', error, {
|
|
79
|
+
promptName: request.params.name,
|
|
80
|
+
availablePrompts: prompts.map(p => p.definition.name),
|
|
81
|
+
});
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
return await prompt.handler(request.params.arguments ?? {});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
logger.error(`Prompt execution failed: ${request.params.name}`, error as Error, {
|
|
89
|
+
promptName: request.params.name,
|
|
90
|
+
errorType: 'PROMPT_EXECUTION_FAILURE',
|
|
91
|
+
});
|
|
92
|
+
throw error;
|
|
58
93
|
}
|
|
59
|
-
return prompt.handler(request.params.arguments ?? {});
|
|
60
94
|
});
|
|
61
95
|
}
|
|
62
96
|
|
|
63
97
|
async start(): Promise<void> {
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
try {
|
|
99
|
+
const transport = new StdioServerTransport();
|
|
100
|
+
await this.server.connect(transport);
|
|
101
|
+
logger.info('MCP server connected successfully via stdio transport');
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.error('Failed to connect MCP server', error as Error, {
|
|
104
|
+
errorType: 'CONNECTION_FAILURE',
|
|
105
|
+
});
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
66
108
|
}
|
|
67
109
|
}
|
package/src/testing/mock-data.ts
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
import type { Solution, FindSolutionResult, Balance } from '../types.js';
|
|
2
|
-
|
|
3
|
-
export const mockSolutions: Solution[] = [
|
|
4
|
-
{
|
|
5
|
-
id: 'sol_001',
|
|
6
|
-
author_id: 'user_123',
|
|
7
|
-
query_title: 'How to implement binary search in TypeScript',
|
|
8
|
-
solution_body: `function binarySearch<T>(arr: T[], target: T): number {
|
|
9
|
-
let left = 0;
|
|
10
|
-
let right = arr.length - 1;
|
|
11
|
-
while (left <= right) {
|
|
12
|
-
const mid = Math.floor((left + right) / 2);
|
|
13
|
-
if (arr[mid] === target) return mid;
|
|
14
|
-
if (arr[mid] < target) left = mid + 1;
|
|
15
|
-
else right = mid - 1;
|
|
16
|
-
}
|
|
17
|
-
return -1;
|
|
18
|
-
}`,
|
|
19
|
-
price_current: 50,
|
|
20
|
-
verification_state: 'VERIFIED',
|
|
21
|
-
access_count: 127,
|
|
22
|
-
upvotes: 45,
|
|
23
|
-
downvotes: 2,
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
id: 'sol_002',
|
|
27
|
-
author_id: 'user_456',
|
|
28
|
-
query_title: 'Fix memory leak in Node.js event listeners',
|
|
29
|
-
solution_body: `// Always remove event listeners when done
|
|
30
|
-
const handler = () => { /* ... */ };
|
|
31
|
-
emitter.on('event', handler);
|
|
32
|
-
// Later:
|
|
33
|
-
emitter.off('event', handler);
|
|
34
|
-
|
|
35
|
-
// Or use once() for one-time listeners
|
|
36
|
-
emitter.once('event', () => { /* ... */ });`,
|
|
37
|
-
price_current: 75,
|
|
38
|
-
verification_state: 'VERIFIED',
|
|
39
|
-
access_count: 89,
|
|
40
|
-
upvotes: 32,
|
|
41
|
-
downvotes: 1,
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: 'sol_003',
|
|
45
|
-
author_id: 'user_789',
|
|
46
|
-
query_title: 'Optimize React re-renders with useMemo',
|
|
47
|
-
solution_body: `import { useMemo } from 'react';
|
|
48
|
-
|
|
49
|
-
function ExpensiveComponent({ data }) {
|
|
50
|
-
const processed = useMemo(() => {
|
|
51
|
-
return data.map(item => heavyComputation(item));
|
|
52
|
-
}, [data]);
|
|
53
|
-
|
|
54
|
-
return <div>{processed}</div>;
|
|
55
|
-
}`,
|
|
56
|
-
price_current: 60,
|
|
57
|
-
verification_state: 'PENDING',
|
|
58
|
-
access_count: 15,
|
|
59
|
-
upvotes: 8,
|
|
60
|
-
downvotes: 0,
|
|
61
|
-
},
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
export const mockFindResults: FindSolutionResult[] = [
|
|
65
|
-
{
|
|
66
|
-
solution_id: 'sol_001',
|
|
67
|
-
query_title: 'How to implement binary search in TypeScript',
|
|
68
|
-
human_verification_required: false,
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
solution_id: 'sol_002',
|
|
72
|
-
query_title: 'Fix memory leak in Node.js event listeners',
|
|
73
|
-
solution_body: `// Always remove event listeners when done
|
|
74
|
-
const handler = () => { /* ... */ };
|
|
75
|
-
emitter.on('event', handler);
|
|
76
|
-
// Later:
|
|
77
|
-
emitter.off('event', handler);
|
|
78
|
-
|
|
79
|
-
// Or use once() for one-time listeners
|
|
80
|
-
emitter.once('event', () => { /* ... */ });`,
|
|
81
|
-
human_verification_required: true,
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
solution_id: 'sol_003',
|
|
85
|
-
query_title: 'Optimize React re-renders with useMemo',
|
|
86
|
-
solution_body: `import { useMemo } from 'react';
|
|
87
|
-
|
|
88
|
-
function ExpensiveComponent({ data }) {
|
|
89
|
-
const processed = useMemo(() => {
|
|
90
|
-
return data.map(item => heavyComputation(item));
|
|
91
|
-
}, [data]);
|
|
92
|
-
|
|
93
|
-
return <div>{processed}</div>;
|
|
94
|
-
}`,
|
|
95
|
-
human_verification_required: false,
|
|
96
|
-
},
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
export const mockBalance: Balance = {
|
|
100
|
-
available: 1500,
|
|
101
|
-
pending_debits: 75,
|
|
102
|
-
pending_credits: 200,
|
|
103
|
-
total_earned: 3500,
|
|
104
|
-
total_spent: 1800,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
export function createMockSolution(overrides: Partial<Solution> = {}): Solution {
|
|
108
|
-
return {
|
|
109
|
-
id: `sol_${Date.now()}`,
|
|
110
|
-
author_id: 'user_mock',
|
|
111
|
-
query_title: 'Mock solution title',
|
|
112
|
-
solution_body: 'Mock solution body content',
|
|
113
|
-
price_current: 50,
|
|
114
|
-
verification_state: 'PENDING',
|
|
115
|
-
access_count: 0,
|
|
116
|
-
upvotes: 0,
|
|
117
|
-
downvotes: 0,
|
|
118
|
-
...overrides,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function createMockFindResult(
|
|
123
|
-
overrides: Partial<FindSolutionResult> = {}
|
|
124
|
-
): FindSolutionResult {
|
|
125
|
-
return {
|
|
126
|
-
solution_id: `sol_${Date.now()}`,
|
|
127
|
-
query_title: 'Mock query title',
|
|
128
|
-
human_verification_required: false,
|
|
129
|
-
...overrides,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function createMockBalance(overrides: Partial<Balance> = {}): Balance {
|
|
134
|
-
return {
|
|
135
|
-
available: 1000,
|
|
136
|
-
pending_debits: 0,
|
|
137
|
-
pending_credits: 0,
|
|
138
|
-
total_earned: 1000,
|
|
139
|
-
total_spent: 0,
|
|
140
|
-
...overrides,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
1
|
+
import type { Solution, FindSolutionResult, Balance } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const mockSolutions: Solution[] = [
|
|
4
|
+
{
|
|
5
|
+
id: 'sol_001',
|
|
6
|
+
author_id: 'user_123',
|
|
7
|
+
query_title: 'How to implement binary search in TypeScript',
|
|
8
|
+
solution_body: `function binarySearch<T>(arr: T[], target: T): number {
|
|
9
|
+
let left = 0;
|
|
10
|
+
let right = arr.length - 1;
|
|
11
|
+
while (left <= right) {
|
|
12
|
+
const mid = Math.floor((left + right) / 2);
|
|
13
|
+
if (arr[mid] === target) return mid;
|
|
14
|
+
if (arr[mid] < target) left = mid + 1;
|
|
15
|
+
else right = mid - 1;
|
|
16
|
+
}
|
|
17
|
+
return -1;
|
|
18
|
+
}`,
|
|
19
|
+
price_current: 50,
|
|
20
|
+
verification_state: 'VERIFIED',
|
|
21
|
+
access_count: 127,
|
|
22
|
+
upvotes: 45,
|
|
23
|
+
downvotes: 2,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'sol_002',
|
|
27
|
+
author_id: 'user_456',
|
|
28
|
+
query_title: 'Fix memory leak in Node.js event listeners',
|
|
29
|
+
solution_body: `// Always remove event listeners when done
|
|
30
|
+
const handler = () => { /* ... */ };
|
|
31
|
+
emitter.on('event', handler);
|
|
32
|
+
// Later:
|
|
33
|
+
emitter.off('event', handler);
|
|
34
|
+
|
|
35
|
+
// Or use once() for one-time listeners
|
|
36
|
+
emitter.once('event', () => { /* ... */ });`,
|
|
37
|
+
price_current: 75,
|
|
38
|
+
verification_state: 'VERIFIED',
|
|
39
|
+
access_count: 89,
|
|
40
|
+
upvotes: 32,
|
|
41
|
+
downvotes: 1,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'sol_003',
|
|
45
|
+
author_id: 'user_789',
|
|
46
|
+
query_title: 'Optimize React re-renders with useMemo',
|
|
47
|
+
solution_body: `import { useMemo } from 'react';
|
|
48
|
+
|
|
49
|
+
function ExpensiveComponent({ data }) {
|
|
50
|
+
const processed = useMemo(() => {
|
|
51
|
+
return data.map(item => heavyComputation(item));
|
|
52
|
+
}, [data]);
|
|
53
|
+
|
|
54
|
+
return <div>{processed}</div>;
|
|
55
|
+
}`,
|
|
56
|
+
price_current: 60,
|
|
57
|
+
verification_state: 'PENDING',
|
|
58
|
+
access_count: 15,
|
|
59
|
+
upvotes: 8,
|
|
60
|
+
downvotes: 0,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
export const mockFindResults: FindSolutionResult[] = [
|
|
65
|
+
{
|
|
66
|
+
solution_id: 'sol_001',
|
|
67
|
+
query_title: 'How to implement binary search in TypeScript',
|
|
68
|
+
human_verification_required: false,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
solution_id: 'sol_002',
|
|
72
|
+
query_title: 'Fix memory leak in Node.js event listeners',
|
|
73
|
+
solution_body: `// Always remove event listeners when done
|
|
74
|
+
const handler = () => { /* ... */ };
|
|
75
|
+
emitter.on('event', handler);
|
|
76
|
+
// Later:
|
|
77
|
+
emitter.off('event', handler);
|
|
78
|
+
|
|
79
|
+
// Or use once() for one-time listeners
|
|
80
|
+
emitter.once('event', () => { /* ... */ });`,
|
|
81
|
+
human_verification_required: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
solution_id: 'sol_003',
|
|
85
|
+
query_title: 'Optimize React re-renders with useMemo',
|
|
86
|
+
solution_body: `import { useMemo } from 'react';
|
|
87
|
+
|
|
88
|
+
function ExpensiveComponent({ data }) {
|
|
89
|
+
const processed = useMemo(() => {
|
|
90
|
+
return data.map(item => heavyComputation(item));
|
|
91
|
+
}, [data]);
|
|
92
|
+
|
|
93
|
+
return <div>{processed}</div>;
|
|
94
|
+
}`,
|
|
95
|
+
human_verification_required: false,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
export const mockBalance: Balance = {
|
|
100
|
+
available: 1500,
|
|
101
|
+
pending_debits: 75,
|
|
102
|
+
pending_credits: 200,
|
|
103
|
+
total_earned: 3500,
|
|
104
|
+
total_spent: 1800,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export function createMockSolution(overrides: Partial<Solution> = {}): Solution {
|
|
108
|
+
return {
|
|
109
|
+
id: `sol_${Date.now()}`,
|
|
110
|
+
author_id: 'user_mock',
|
|
111
|
+
query_title: 'Mock solution title',
|
|
112
|
+
solution_body: 'Mock solution body content',
|
|
113
|
+
price_current: 50,
|
|
114
|
+
verification_state: 'PENDING',
|
|
115
|
+
access_count: 0,
|
|
116
|
+
upvotes: 0,
|
|
117
|
+
downvotes: 0,
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createMockFindResult(
|
|
123
|
+
overrides: Partial<FindSolutionResult> = {}
|
|
124
|
+
): FindSolutionResult {
|
|
125
|
+
return {
|
|
126
|
+
solution_id: `sol_${Date.now()}`,
|
|
127
|
+
query_title: 'Mock query title',
|
|
128
|
+
human_verification_required: false,
|
|
129
|
+
...overrides,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createMockBalance(overrides: Partial<Balance> = {}): Balance {
|
|
134
|
+
return {
|
|
135
|
+
available: 1000,
|
|
136
|
+
pending_debits: 0,
|
|
137
|
+
pending_credits: 0,
|
|
138
|
+
total_earned: 1000,
|
|
139
|
+
total_spent: 0,
|
|
140
|
+
...overrides,
|
|
141
|
+
};
|
|
142
|
+
}
|