···
78
78
setImmediate: 'readonly',
79
79
setInterval: 'readonly',
80
80
clearInterval: 'readonly',
81
81
+
URL: 'readonly',
81
82
},
82
83
},
83
84
rules: {
···
3
3
import { IMetadataService } from '../../../cards/domain/services/IMetadataService';
4
4
import { UrlMetadata } from '../../../cards/domain/value-objects/UrlMetadata';
5
5
import { URL } from '../../../cards/domain/value-objects/URL';
6
6
-
import { ILeafletSearchService, LeafletDocumentResult } from './ILeafletSearchService';
6
6
+
import {
7
7
+
ILeafletSearchService,
8
8
+
LeafletDocumentResult,
9
9
+
} from './ILeafletSearchService';
7
10
const atpi = require('atpi');
8
11
9
12
export interface LeafletLinkingRecord {
···
36
39
preferences?: any;
37
40
}
38
41
39
39
-
export class ConstellationLeafletSearchService implements ILeafletSearchService {
42
42
+
export class ConstellationLeafletSearchService
43
43
+
implements ILeafletSearchService
44
44
+
{
40
45
private readonly CONSTELLATION_BASE_URL =
41
46
'https://constellation.microcosm.blue';
42
47
···
77
82
});
78
83
79
84
const documentResults = await Promise.all(documentPromises);
80
80
-
85
85
+
81
86
// Filter out null results
82
87
for (const result of documentResults) {
83
88
if (result !== null) {
···
1
1
import Redis from 'ioredis';
2
2
-
import { ILeafletSearchService, LeafletDocumentResult } from '../domain/services/ILeafletSearchService';
2
2
+
import {
3
3
+
ILeafletSearchService,
4
4
+
LeafletDocumentResult,
5
5
+
} from '../domain/services/ILeafletSearchService';
3
6
import { Result, ok } from '../../../shared/core/Result';
4
7
import { AppError } from '../../../shared/core/AppError';
5
8
···
74
77
}
75
78
}
76
79
77
77
-
private getCacheKey(targetUrl: string, limit?: number, cursor?: string): string {
80
80
+
private getCacheKey(
81
81
+
targetUrl: string,
82
82
+
limit?: number,
83
83
+
cursor?: string,
84
84
+
): string {
78
85
// Create a deterministic cache key that includes all parameters
79
86
const params = [
80
87
targetUrl,
81
88
limit?.toString() || 'no_limit',
82
89
cursor || 'no_cursor',
83
90
].join('|');
84
84
-
91
91
+
85
92
return `${this.CACHE_KEY_PREFIX}${params}`;
86
93
}
87
94
···
94
101
// we'll use a pattern to delete all keys for this URL
95
102
const pattern = `${this.CACHE_KEY_PREFIX}${targetUrl}|*`;
96
103
const keys = await this.redis.keys(pattern);
97
97
-
104
104
+
98
105
if (keys.length > 0) {
99
106
await this.redis.del(...keys);
100
107
}
···
113
120
try {
114
121
const pattern = `${this.CACHE_KEY_PREFIX}*`;
115
122
const keys = await this.redis.keys(pattern);
116
116
-
123
123
+
117
124
if (keys.length > 0) {
118
125
await this.redis.del(...keys);
119
126
}
···
125
132
/**
126
133
* Warm the cache by pre-fetching results for a URL
127
134
*/
128
128
-
async warmCache(targetUrl: string, limit?: number, cursor?: string): Promise<void> {
135
135
+
async warmCache(
136
136
+
targetUrl: string,
137
137
+
limit?: number,
138
138
+
cursor?: string,
139
139
+
): Promise<void> {
129
140
await this.searchLeafletDocsForUrl(targetUrl, limit, cursor);
130
141
}
131
142
}
···
1
1
import { Result, ok } from '../../../shared/core/Result';
2
2
import { AppError } from '../../../shared/core/AppError';
3
3
-
import { ILeafletSearchService, LeafletDocumentResult } from '../domain/services/ILeafletSearchService';
3
3
+
import {
4
4
+
ILeafletSearchService,
5
5
+
LeafletDocumentResult,
6
6
+
} from '../domain/services/ILeafletSearchService';
4
7
import { UrlMetadata } from '../../cards/domain/value-objects/UrlMetadata';
5
8
import { UrlType } from '../../cards/domain/value-objects/UrlType';
6
9
7
10
export class FakeLeafletSearchService implements ILeafletSearchService {
8
8
-
private readonly mockResults: Map<string, LeafletDocumentResult[]> = new Map();
11
11
+
private readonly mockResults: Map<string, LeafletDocumentResult[]> =
12
12
+
new Map();
9
13
10
14
constructor() {
11
15
// Pre-populate with some mock data
···
18
22
cursor?: string,
19
23
): Promise<Result<LeafletDocumentResult[], AppError.UnexpectedError>> {
20
24
// Simulate some processing time
21
21
-
await new Promise(resolve => setTimeout(resolve, 100));
25
25
+
await new Promise((resolve) => setTimeout(resolve, 100));
26
26
+
27
27
+
const results =
28
28
+
this.mockResults.get(targetUrl) || this.generateMockResults(targetUrl);
22
29
23
23
-
const results = this.mockResults.get(targetUrl) || this.generateMockResults(targetUrl);
24
24
-
25
30
// Apply limit if specified
26
31
const limitedResults = limit ? results.slice(0, limit) : results;
27
27
-
32
32
+
28
33
return ok(limitedResults);
29
34
}
30
35
···
46
51
url: 'https://academic.leaflet.com/document-456',
47
52
},
48
53
]);
49
49
-
54
54
+
50
55
this.mockResults.set('https://example.com', exampleResults);
51
56
52
57
// Mock results for science.org URL (commonly used in tests)
53
53
-
const scienceResults = this.createMockResults('https://www.science.org/doi/10.1126/science.adt7790', [
54
54
-
{
55
55
-
title: 'Commentary on Climate Research',
56
56
-
author: 'Dr. Climate Researcher',
57
57
-
description: 'Analysis of the latest climate science findings',
58
58
-
siteName: 'Climate Leaflet',
59
59
-
url: 'https://climate.leaflet.com/commentary-789',
60
60
-
},
61
61
-
]);
62
62
-
63
63
-
this.mockResults.set('https://www.science.org/doi/10.1126/science.adt7790', scienceResults);
58
58
+
const scienceResults = this.createMockResults(
59
59
+
'https://www.science.org/doi/10.1126/science.adt7790',
60
60
+
[
61
61
+
{
62
62
+
title: 'Commentary on Climate Research',
63
63
+
author: 'Dr. Climate Researcher',
64
64
+
description: 'Analysis of the latest climate science findings',
65
65
+
siteName: 'Climate Leaflet',
66
66
+
url: 'https://climate.leaflet.com/commentary-789',
67
67
+
},
68
68
+
],
69
69
+
);
70
70
+
71
71
+
this.mockResults.set(
72
72
+
'https://www.science.org/doi/10.1126/science.adt7790',
73
73
+
scienceResults,
74
74
+
);
64
75
}
65
76
66
77
private generateMockResults(targetUrl: string): LeafletDocumentResult[] {
···
83
94
return results;
84
95
}
85
96
86
86
-
private createMockResults(targetUrl: string, mockData: Array<{
87
87
-
title: string;
88
88
-
author: string;
89
89
-
description: string;
90
90
-
siteName: string;
91
91
-
url: string;
92
92
-
}>): LeafletDocumentResult[] {
93
93
-
return mockData.map(data => {
97
97
+
private createMockResults(
98
98
+
targetUrl: string,
99
99
+
mockData: Array<{
100
100
+
title: string;
101
101
+
author: string;
102
102
+
description: string;
103
103
+
siteName: string;
104
104
+
url: string;
105
105
+
}>,
106
106
+
): LeafletDocumentResult[] {
107
107
+
return mockData.map((data) => {
94
108
const metadataResult = UrlMetadata.create({
95
109
url: data.url,
96
110
title: data.title,
···
98
112
description: data.description,
99
113
siteName: data.siteName,
100
114
type: UrlType.ARTICLE,
101
101
-
publishedDate: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000), // Random date within last year
115
115
+
publishedDate: new Date(
116
116
+
Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000,
117
117
+
), // Random date within last year
102
118
retrievedAt: new Date(),
103
119
});
104
120
105
121
if (metadataResult.isErr()) {
106
106
-
throw new Error(`Failed to create mock metadata: ${metadataResult.error.message}`);
122
122
+
throw new Error(
123
123
+
`Failed to create mock metadata: ${metadataResult.error.message}`,
124
124
+
);
107
125
}
108
126
109
127
return {
···
61
61
import { InMemoryVectorDatabase } from '../../../../modules/search/infrastructure/InMemoryVectorDatabase';
62
62
import { UpstashVectorDatabase } from '../../../../modules/search/infrastructure/UpstashVectorDatabase';
63
63
import { NotificationService } from '../../../../modules/notifications/domain/services/NotificationService';
64
64
+
import { FakeLeafletSearchService } from 'src/modules/search/infrastructure/FakeLeafletSearchService';
65
65
+
import { ILeafletSearchService } from 'src/modules/search/domain/services/ILeafletSearchService';
66
66
+
import { ConstellationLeafletSearchService } from 'src/modules/search/domain/services/ConstellationLeafletSearchService';
67
67
+
import { CachedLeafletSearchService } from 'src/modules/search/infrastructure/CachedLeafletSearchService';
64
68
65
69
// Shared services needed by both web app and workers
66
70
export interface SharedServices {
···
358
362
359
363
// Create LeafletSearchService with caching
360
364
let leafletSearchService: ILeafletSearchService;
361
361
-
365
365
+
362
366
if (useMockPersistence) {
363
367
// Use fake implementation for mock persistence
364
368
leafletSearchService = new FakeLeafletSearchService();
365
369
} else {
366
370
// Use real Constellation service with caching
367
367
-
const baseLeafletSearchService = new ConstellationLeafletSearchService(metadataService);
371
371
+
const baseLeafletSearchService = new ConstellationLeafletSearchService(
372
372
+
metadataService,
373
373
+
);
368
374
const redisConfig = configService.getRedisConfig();
369
375
const redis = RedisFactory.createConnection(redisConfig);
370
376
leafletSearchService = new CachedLeafletSearchService(
···
436
442
};
437
443
}
438
444
}
439
439
-
import { ConstellationLeafletSearchService } from '../../../../modules/search/domain/services/LeafletSearchService';
440
440
-
import { FakeLeafletSearchService } from '../../../../modules/search/infrastructure/FakeLeafletSearchService';
441
441
-
import { CachedLeafletSearchService } from '../../../../modules/search/infrastructure/CachedLeafletSearchService';
442
442
-
import { ILeafletSearchService } from '../../../../modules/search/domain/services/ILeafletSearchService';