simile-search 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.
- package/README.md +74 -1
- package/dist/ann.d.ts +110 -0
- package/dist/ann.js +374 -0
- package/dist/cache.d.ts +94 -0
- package/dist/cache.js +179 -0
- package/dist/embedder.d.ts +55 -4
- package/dist/embedder.js +144 -12
- package/dist/engine.d.ts +16 -3
- package/dist/engine.js +164 -20
- package/dist/engine.test.js +49 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/quantization.d.ts +50 -0
- package/dist/quantization.js +271 -0
- package/dist/similarity.d.ts +24 -0
- package/dist/similarity.js +105 -0
- package/dist/types.d.ts +35 -0
- package/dist/updater.d.ts +172 -0
- package/dist/updater.js +336 -0
- package/package.json +1 -1
package/dist/similarity.js
CHANGED
|
@@ -10,6 +10,94 @@ export function cosine(a, b) {
|
|
|
10
10
|
dot += a[i] * b[i];
|
|
11
11
|
return dot;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* SIMD-style unrolled cosine similarity for better performance.
|
|
15
|
+
* Processes 4 elements at a time for ~2-4x speedup.
|
|
16
|
+
*/
|
|
17
|
+
export function cosineFast(a, b) {
|
|
18
|
+
const len = a.length;
|
|
19
|
+
let dot0 = 0, dot1 = 0, dot2 = 0, dot3 = 0;
|
|
20
|
+
// Process 4 elements at a time
|
|
21
|
+
const end4 = len - (len % 4);
|
|
22
|
+
for (let i = 0; i < end4; i += 4) {
|
|
23
|
+
dot0 += a[i] * b[i];
|
|
24
|
+
dot1 += a[i + 1] * b[i + 1];
|
|
25
|
+
dot2 += a[i + 2] * b[i + 2];
|
|
26
|
+
dot3 += a[i + 3] * b[i + 3];
|
|
27
|
+
}
|
|
28
|
+
// Handle remaining elements
|
|
29
|
+
let dot = dot0 + dot1 + dot2 + dot3;
|
|
30
|
+
for (let i = end4; i < len; i++) {
|
|
31
|
+
dot += a[i] * b[i];
|
|
32
|
+
}
|
|
33
|
+
return dot;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Early-exit cosine similarity with threshold.
|
|
37
|
+
* Returns null if the result would definitely be below threshold.
|
|
38
|
+
* Useful for filtering out low-scoring candidates quickly.
|
|
39
|
+
*/
|
|
40
|
+
export function cosineWithThreshold(a, b, threshold) {
|
|
41
|
+
const len = a.length;
|
|
42
|
+
let dot = 0;
|
|
43
|
+
// Check partial result periodically (every 64 elements)
|
|
44
|
+
const checkInterval = 64;
|
|
45
|
+
const remainingMultiplier = 1.0; // Assume best case for remaining
|
|
46
|
+
for (let i = 0; i < len; i++) {
|
|
47
|
+
dot += a[i] * b[i];
|
|
48
|
+
// Early termination check
|
|
49
|
+
if ((i + 1) % checkInterval === 0 && i < len - 1) {
|
|
50
|
+
// Estimate max possible final score
|
|
51
|
+
const progress = (i + 1) / len;
|
|
52
|
+
const maxPossible = dot + (1 - progress) * remainingMultiplier;
|
|
53
|
+
if (maxPossible < threshold) {
|
|
54
|
+
return null; // Cannot possibly reach threshold
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return dot;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Batch cosine similarity with built-in top-K selection.
|
|
62
|
+
* More efficient than computing all similarities then sorting.
|
|
63
|
+
*/
|
|
64
|
+
export function batchCosine(query, vectors, topK, threshold = 0) {
|
|
65
|
+
// For small sets, use simple approach
|
|
66
|
+
if (vectors.length <= topK * 2) {
|
|
67
|
+
const results = vectors.map((v, i) => ({
|
|
68
|
+
index: i,
|
|
69
|
+
score: cosineFast(query, v),
|
|
70
|
+
}));
|
|
71
|
+
return results
|
|
72
|
+
.filter(r => r.score >= threshold)
|
|
73
|
+
.sort((a, b) => b.score - a.score)
|
|
74
|
+
.slice(0, topK);
|
|
75
|
+
}
|
|
76
|
+
// For larger sets, use min-heap approach
|
|
77
|
+
const results = [];
|
|
78
|
+
let minScore = threshold;
|
|
79
|
+
for (let i = 0; i < vectors.length; i++) {
|
|
80
|
+
// Try early exit
|
|
81
|
+
const score = cosineWithThreshold(query, vectors[i], minScore);
|
|
82
|
+
if (score === null)
|
|
83
|
+
continue;
|
|
84
|
+
if (results.length < topK) {
|
|
85
|
+
results.push({ index: i, score });
|
|
86
|
+
if (results.length === topK) {
|
|
87
|
+
// Sort once and track minimum
|
|
88
|
+
results.sort((a, b) => b.score - a.score);
|
|
89
|
+
minScore = Math.max(minScore, results[results.length - 1].score);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else if (score > minScore) {
|
|
93
|
+
// Replace minimum
|
|
94
|
+
results[results.length - 1] = { index: i, score };
|
|
95
|
+
results.sort((a, b) => b.score - a.score);
|
|
96
|
+
minScore = results[results.length - 1].score;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return results.sort((a, b) => b.score - a.score);
|
|
100
|
+
}
|
|
13
101
|
/**
|
|
14
102
|
* Compute fuzzy similarity score using Levenshtein distance.
|
|
15
103
|
* Returns a value between 0 and 1, where 1 is an exact match.
|
|
@@ -35,6 +123,23 @@ export function keywordScore(query, text) {
|
|
|
35
123
|
const hits = queryWords.filter((w) => textLower.includes(w)).length;
|
|
36
124
|
return hits / queryWords.length;
|
|
37
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Fast keyword score with early exit.
|
|
128
|
+
* Stops as soon as all query words are found.
|
|
129
|
+
*/
|
|
130
|
+
export function keywordScoreFast(query, text) {
|
|
131
|
+
const queryWords = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
132
|
+
if (queryWords.length === 0)
|
|
133
|
+
return 0;
|
|
134
|
+
const textLower = text.toLowerCase();
|
|
135
|
+
let hits = 0;
|
|
136
|
+
for (const word of queryWords) {
|
|
137
|
+
if (textLower.includes(word)) {
|
|
138
|
+
hits++;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return hits / queryWords.length;
|
|
142
|
+
}
|
|
38
143
|
/**
|
|
39
144
|
* Calculate min/max statistics for score normalization.
|
|
40
145
|
*/
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { HNSWConfig } from "./ann.js";
|
|
2
|
+
import { CacheOptions, CacheStats } from "./cache.js";
|
|
3
|
+
import { QuantizationType } from "./quantization.js";
|
|
4
|
+
import { UpdaterConfig } from "./updater.js";
|
|
5
|
+
export { HNSWConfig, CacheOptions, CacheStats, QuantizationType, UpdaterConfig };
|
|
1
6
|
export interface SearchItem<T = any> {
|
|
2
7
|
id: string;
|
|
3
8
|
text: string;
|
|
@@ -28,6 +33,10 @@ export interface SearchOptions {
|
|
|
28
33
|
threshold?: number;
|
|
29
34
|
/** Minimum query length to trigger search (default: 1) */
|
|
30
35
|
minLength?: number;
|
|
36
|
+
/** Use fast similarity computation (default: true for large datasets) */
|
|
37
|
+
useFastSimilarity?: boolean;
|
|
38
|
+
/** Use ANN index if available (default: true) */
|
|
39
|
+
useANN?: boolean;
|
|
31
40
|
}
|
|
32
41
|
export interface HybridWeights {
|
|
33
42
|
/** Semantic similarity weight (0-1), default: 0.7 */
|
|
@@ -50,6 +59,14 @@ export interface SimileConfig {
|
|
|
50
59
|
textPaths?: string[];
|
|
51
60
|
/** Whether to normalize scores across different scoring methods (default: true) */
|
|
52
61
|
normalizeScores?: boolean;
|
|
62
|
+
/** Enable vector caching (default: true) */
|
|
63
|
+
cache?: boolean | CacheOptions;
|
|
64
|
+
/** Vector quantization type (default: 'float32') */
|
|
65
|
+
quantization?: QuantizationType;
|
|
66
|
+
/** Enable ANN index for large datasets (default: auto based on size) */
|
|
67
|
+
useANN?: boolean | HNSWConfig;
|
|
68
|
+
/** Minimum items to automatically enable ANN (default: 1000) */
|
|
69
|
+
annThreshold?: number;
|
|
53
70
|
}
|
|
54
71
|
/** Serialized state for persistence */
|
|
55
72
|
export interface SimileSnapshot<T = any> {
|
|
@@ -61,4 +78,22 @@ export interface SimileSnapshot<T = any> {
|
|
|
61
78
|
createdAt: string;
|
|
62
79
|
/** Text paths used for extraction */
|
|
63
80
|
textPaths?: string[];
|
|
81
|
+
/** Quantization type used */
|
|
82
|
+
quantization?: QuantizationType;
|
|
83
|
+
/** Serialized ANN index */
|
|
84
|
+
annIndex?: any;
|
|
85
|
+
/** Serialized cache */
|
|
86
|
+
cache?: any;
|
|
87
|
+
}
|
|
88
|
+
export interface IndexInfo {
|
|
89
|
+
type: 'linear' | 'hnsw';
|
|
90
|
+
size: number;
|
|
91
|
+
memory: string;
|
|
92
|
+
annStats?: {
|
|
93
|
+
size: number;
|
|
94
|
+
levels: number;
|
|
95
|
+
avgConnections: number;
|
|
96
|
+
memoryBytes: number;
|
|
97
|
+
};
|
|
98
|
+
cacheStats?: CacheStats;
|
|
64
99
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Updater - Non-blocking updates with queue-based processing.
|
|
3
|
+
*
|
|
4
|
+
* Allows adding items asynchronously without blocking search operations.
|
|
5
|
+
* Items are batched and processed in the background for efficiency.
|
|
6
|
+
*/
|
|
7
|
+
import { SearchItem } from "./types.js";
|
|
8
|
+
export interface UpdaterConfig {
|
|
9
|
+
/** Milliseconds to wait before processing queued items (default: 100) */
|
|
10
|
+
batchDelay?: number;
|
|
11
|
+
/** Maximum items to process in a single batch (default: 50) */
|
|
12
|
+
maxBatchSize?: number;
|
|
13
|
+
/** Progress callback */
|
|
14
|
+
onProgress?: (processed: number, total: number) => void;
|
|
15
|
+
/** Error callback */
|
|
16
|
+
onError?: (error: Error, item: SearchItem) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface UpdaterStats {
|
|
19
|
+
/** Total items processed since creation */
|
|
20
|
+
totalProcessed: number;
|
|
21
|
+
/** Current queue size */
|
|
22
|
+
pendingCount: number;
|
|
23
|
+
/** Whether currently processing */
|
|
24
|
+
isProcessing: boolean;
|
|
25
|
+
/** Average items per batch */
|
|
26
|
+
avgBatchSize: number;
|
|
27
|
+
/** Total batches processed */
|
|
28
|
+
batchCount: number;
|
|
29
|
+
}
|
|
30
|
+
type UpdateEventType = 'complete' | 'error' | 'batch' | 'progress';
|
|
31
|
+
type UpdateEventHandler = (...args: any[]) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Interface for the Simile engine (to avoid circular dependencies).
|
|
34
|
+
*/
|
|
35
|
+
interface SimileInterface<T> {
|
|
36
|
+
add(items: SearchItem<T>[]): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Background updater for non-blocking item additions.
|
|
40
|
+
*/
|
|
41
|
+
export declare class BackgroundUpdater<T = any> {
|
|
42
|
+
private simile;
|
|
43
|
+
private config;
|
|
44
|
+
private _queue;
|
|
45
|
+
private processing;
|
|
46
|
+
private timeoutId;
|
|
47
|
+
private eventHandlers;
|
|
48
|
+
private totalProcessed;
|
|
49
|
+
private batchCount;
|
|
50
|
+
private totalBatchItems;
|
|
51
|
+
constructor(simile: SimileInterface<T>, config?: UpdaterConfig);
|
|
52
|
+
/**
|
|
53
|
+
* Queue items for background embedding.
|
|
54
|
+
* Items are batched and processed after batchDelay ms.
|
|
55
|
+
*/
|
|
56
|
+
enqueue(items: SearchItem<T>[]): void;
|
|
57
|
+
/**
|
|
58
|
+
* Queue a single item.
|
|
59
|
+
*/
|
|
60
|
+
enqueueOne(item: SearchItem<T>): void;
|
|
61
|
+
/**
|
|
62
|
+
* Force immediate processing of queued items.
|
|
63
|
+
*/
|
|
64
|
+
flush(): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Wait for all pending items to be processed.
|
|
67
|
+
*/
|
|
68
|
+
waitForCompletion(): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Get the number of items waiting to be processed.
|
|
71
|
+
*/
|
|
72
|
+
getPendingCount(): number;
|
|
73
|
+
/**
|
|
74
|
+
* Check if currently processing.
|
|
75
|
+
*/
|
|
76
|
+
isProcessing(): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Clear all pending items without processing.
|
|
79
|
+
*/
|
|
80
|
+
clear(): void;
|
|
81
|
+
/**
|
|
82
|
+
* Get updater statistics.
|
|
83
|
+
*/
|
|
84
|
+
getStats(): UpdaterStats;
|
|
85
|
+
/**
|
|
86
|
+
* Reset statistics.
|
|
87
|
+
*/
|
|
88
|
+
resetStats(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Register an event handler.
|
|
91
|
+
*/
|
|
92
|
+
on(event: UpdateEventType, handler: UpdateEventHandler): void;
|
|
93
|
+
/**
|
|
94
|
+
* Remove an event handler.
|
|
95
|
+
*/
|
|
96
|
+
off(event: UpdateEventType, handler: UpdateEventHandler): void;
|
|
97
|
+
/**
|
|
98
|
+
* Emit an event to all registered handlers.
|
|
99
|
+
*/
|
|
100
|
+
private emit;
|
|
101
|
+
private scheduleProcessing;
|
|
102
|
+
private processQueue;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Debounced updater - coalesces rapid updates.
|
|
106
|
+
* Useful when items change frequently (e.g., user typing).
|
|
107
|
+
*/
|
|
108
|
+
export declare class DebouncedUpdater<T = any> {
|
|
109
|
+
private updater;
|
|
110
|
+
private debounceMs;
|
|
111
|
+
private pending;
|
|
112
|
+
private timeoutId;
|
|
113
|
+
constructor(simile: SimileInterface<T>, debounceMs?: number, config?: UpdaterConfig);
|
|
114
|
+
/**
|
|
115
|
+
* Queue an item for update. If same ID is queued again before flush,
|
|
116
|
+
* only the latest version is processed.
|
|
117
|
+
*/
|
|
118
|
+
update(item: SearchItem<T>): void;
|
|
119
|
+
/**
|
|
120
|
+
* Force immediate flush of pending items.
|
|
121
|
+
*/
|
|
122
|
+
flush(): Promise<void>;
|
|
123
|
+
private scheduleFlush;
|
|
124
|
+
private doFlush;
|
|
125
|
+
/**
|
|
126
|
+
* Get pending count (not yet flushed + in queue).
|
|
127
|
+
*/
|
|
128
|
+
getPendingCount(): number;
|
|
129
|
+
/**
|
|
130
|
+
* Clear all pending updates.
|
|
131
|
+
*/
|
|
132
|
+
clear(): void;
|
|
133
|
+
/**
|
|
134
|
+
* Get the underlying updater for event handling.
|
|
135
|
+
*/
|
|
136
|
+
getUpdater(): BackgroundUpdater<T>;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Priority queue for updates - high priority items processed first.
|
|
140
|
+
*/
|
|
141
|
+
export declare class PriorityUpdater<T = any> {
|
|
142
|
+
private simile;
|
|
143
|
+
private config;
|
|
144
|
+
private highPriority;
|
|
145
|
+
private normalPriority;
|
|
146
|
+
private processing;
|
|
147
|
+
constructor(simile: SimileInterface<T>, config?: UpdaterConfig);
|
|
148
|
+
/**
|
|
149
|
+
* Queue high priority item (processed first).
|
|
150
|
+
*/
|
|
151
|
+
queueHigh(items: SearchItem<T>[]): void;
|
|
152
|
+
/**
|
|
153
|
+
* Queue normal priority item.
|
|
154
|
+
*/
|
|
155
|
+
enqueue(items: SearchItem<T>[]): void;
|
|
156
|
+
private timeoutId;
|
|
157
|
+
private scheduleProcessing;
|
|
158
|
+
private processQueue;
|
|
159
|
+
/**
|
|
160
|
+
* Force immediate processing.
|
|
161
|
+
*/
|
|
162
|
+
flush(): Promise<void>;
|
|
163
|
+
/**
|
|
164
|
+
* Get total pending count.
|
|
165
|
+
*/
|
|
166
|
+
getPendingCount(): number;
|
|
167
|
+
/**
|
|
168
|
+
* Clear all pending items.
|
|
169
|
+
*/
|
|
170
|
+
clear(): void;
|
|
171
|
+
}
|
|
172
|
+
export {};
|
package/dist/updater.js
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Updater - Non-blocking updates with queue-based processing.
|
|
3
|
+
*
|
|
4
|
+
* Allows adding items asynchronously without blocking search operations.
|
|
5
|
+
* Items are batched and processed in the background for efficiency.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Background updater for non-blocking item additions.
|
|
9
|
+
*/
|
|
10
|
+
export class BackgroundUpdater {
|
|
11
|
+
constructor(simile, config = {}) {
|
|
12
|
+
this._queue = [];
|
|
13
|
+
this.processing = false;
|
|
14
|
+
this.timeoutId = null;
|
|
15
|
+
this.eventHandlers = new Map();
|
|
16
|
+
// Stats
|
|
17
|
+
this.totalProcessed = 0;
|
|
18
|
+
this.batchCount = 0;
|
|
19
|
+
this.totalBatchItems = 0;
|
|
20
|
+
this.simile = simile;
|
|
21
|
+
this.config = {
|
|
22
|
+
batchDelay: config.batchDelay ?? 100,
|
|
23
|
+
maxBatchSize: config.maxBatchSize ?? 50,
|
|
24
|
+
onProgress: config.onProgress,
|
|
25
|
+
onError: config.onError,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Queue items for background embedding.
|
|
30
|
+
* Items are batched and processed after batchDelay ms.
|
|
31
|
+
*/
|
|
32
|
+
enqueue(items) {
|
|
33
|
+
this._queue.push(...items);
|
|
34
|
+
this.scheduleProcessing();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Queue a single item.
|
|
38
|
+
*/
|
|
39
|
+
enqueueOne(item) {
|
|
40
|
+
this._queue.push(item);
|
|
41
|
+
this.scheduleProcessing();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Force immediate processing of queued items.
|
|
45
|
+
*/
|
|
46
|
+
async flush() {
|
|
47
|
+
if (this.timeoutId) {
|
|
48
|
+
clearTimeout(this.timeoutId);
|
|
49
|
+
this.timeoutId = null;
|
|
50
|
+
}
|
|
51
|
+
await this.processQueue();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Wait for all pending items to be processed.
|
|
55
|
+
*/
|
|
56
|
+
async waitForCompletion() {
|
|
57
|
+
while (this._queue.length > 0 || this.processing) {
|
|
58
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get the number of items waiting to be processed.
|
|
63
|
+
*/
|
|
64
|
+
getPendingCount() {
|
|
65
|
+
return this._queue.length;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Check if currently processing.
|
|
69
|
+
*/
|
|
70
|
+
isProcessing() {
|
|
71
|
+
return this.processing;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Clear all pending items without processing.
|
|
75
|
+
*/
|
|
76
|
+
clear() {
|
|
77
|
+
if (this.timeoutId) {
|
|
78
|
+
clearTimeout(this.timeoutId);
|
|
79
|
+
this.timeoutId = null;
|
|
80
|
+
}
|
|
81
|
+
this._queue = [];
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get updater statistics.
|
|
85
|
+
*/
|
|
86
|
+
getStats() {
|
|
87
|
+
return {
|
|
88
|
+
totalProcessed: this.totalProcessed,
|
|
89
|
+
pendingCount: this._queue.length,
|
|
90
|
+
isProcessing: this.processing,
|
|
91
|
+
avgBatchSize: this.batchCount > 0 ? this.totalBatchItems / this.batchCount : 0,
|
|
92
|
+
batchCount: this.batchCount,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Reset statistics.
|
|
97
|
+
*/
|
|
98
|
+
resetStats() {
|
|
99
|
+
this.totalProcessed = 0;
|
|
100
|
+
this.batchCount = 0;
|
|
101
|
+
this.totalBatchItems = 0;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Register an event handler.
|
|
105
|
+
*/
|
|
106
|
+
on(event, handler) {
|
|
107
|
+
if (!this.eventHandlers.has(event)) {
|
|
108
|
+
this.eventHandlers.set(event, new Set());
|
|
109
|
+
}
|
|
110
|
+
this.eventHandlers.get(event).add(handler);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Remove an event handler.
|
|
114
|
+
*/
|
|
115
|
+
off(event, handler) {
|
|
116
|
+
this.eventHandlers.get(event)?.delete(handler);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Emit an event to all registered handlers.
|
|
120
|
+
*/
|
|
121
|
+
emit(event, ...args) {
|
|
122
|
+
const handlers = this.eventHandlers.get(event);
|
|
123
|
+
if (handlers) {
|
|
124
|
+
for (const handler of handlers) {
|
|
125
|
+
try {
|
|
126
|
+
handler(...args);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
console.error(`Error in ${event} handler:`, e);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
scheduleProcessing() {
|
|
135
|
+
if (this.timeoutId || this.processing)
|
|
136
|
+
return;
|
|
137
|
+
this.timeoutId = setTimeout(() => {
|
|
138
|
+
this.timeoutId = null;
|
|
139
|
+
this.processQueue().catch(console.error);
|
|
140
|
+
}, this.config.batchDelay);
|
|
141
|
+
}
|
|
142
|
+
async processQueue() {
|
|
143
|
+
if (this.processing || this._queue.length === 0)
|
|
144
|
+
return;
|
|
145
|
+
this.processing = true;
|
|
146
|
+
const total = this._queue.length;
|
|
147
|
+
try {
|
|
148
|
+
while (this._queue.length > 0) {
|
|
149
|
+
// Take batch from queue
|
|
150
|
+
const batch = this._queue.splice(0, this.config.maxBatchSize);
|
|
151
|
+
const processed = total - this._queue.length;
|
|
152
|
+
this.emit('batch', batch, processed, total);
|
|
153
|
+
try {
|
|
154
|
+
await this.simile.add(batch);
|
|
155
|
+
this.totalProcessed += batch.length;
|
|
156
|
+
this.batchCount++;
|
|
157
|
+
this.totalBatchItems += batch.length;
|
|
158
|
+
if (this.config.onProgress) {
|
|
159
|
+
this.config.onProgress(processed, total);
|
|
160
|
+
}
|
|
161
|
+
this.emit('progress', processed, total);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// Handle errors per-item if possible
|
|
165
|
+
for (const item of batch) {
|
|
166
|
+
if (this.config.onError) {
|
|
167
|
+
this.config.onError(error, item);
|
|
168
|
+
}
|
|
169
|
+
this.emit('error', error, item);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
this.emit('complete', this.totalProcessed);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
this.processing = false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Debounced updater - coalesces rapid updates.
|
|
182
|
+
* Useful when items change frequently (e.g., user typing).
|
|
183
|
+
*/
|
|
184
|
+
export class DebouncedUpdater {
|
|
185
|
+
constructor(simile, debounceMs = 300, config = {}) {
|
|
186
|
+
this.pending = new Map();
|
|
187
|
+
this.timeoutId = null;
|
|
188
|
+
this.updater = new BackgroundUpdater(simile, config);
|
|
189
|
+
this.debounceMs = debounceMs;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Queue an item for update. If same ID is queued again before flush,
|
|
193
|
+
* only the latest version is processed.
|
|
194
|
+
*/
|
|
195
|
+
update(item) {
|
|
196
|
+
this.pending.set(item.id, item);
|
|
197
|
+
this.scheduleFlush();
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Force immediate flush of pending items.
|
|
201
|
+
*/
|
|
202
|
+
async flush() {
|
|
203
|
+
if (this.timeoutId) {
|
|
204
|
+
clearTimeout(this.timeoutId);
|
|
205
|
+
this.timeoutId = null;
|
|
206
|
+
}
|
|
207
|
+
await this.doFlush();
|
|
208
|
+
}
|
|
209
|
+
scheduleFlush() {
|
|
210
|
+
if (this.timeoutId) {
|
|
211
|
+
clearTimeout(this.timeoutId);
|
|
212
|
+
}
|
|
213
|
+
this.timeoutId = setTimeout(() => {
|
|
214
|
+
this.timeoutId = null;
|
|
215
|
+
this.doFlush().catch(console.error);
|
|
216
|
+
}, this.debounceMs);
|
|
217
|
+
}
|
|
218
|
+
async doFlush() {
|
|
219
|
+
const items = Array.from(this.pending.values());
|
|
220
|
+
this.pending.clear();
|
|
221
|
+
if (items.length > 0) {
|
|
222
|
+
this.updater.enqueue(items);
|
|
223
|
+
await this.updater.flush();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get pending count (not yet flushed + in queue).
|
|
228
|
+
*/
|
|
229
|
+
getPendingCount() {
|
|
230
|
+
return this.pending.size + this.updater.getPendingCount();
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Clear all pending updates.
|
|
234
|
+
*/
|
|
235
|
+
clear() {
|
|
236
|
+
if (this.timeoutId) {
|
|
237
|
+
clearTimeout(this.timeoutId);
|
|
238
|
+
this.timeoutId = null;
|
|
239
|
+
}
|
|
240
|
+
this.pending.clear();
|
|
241
|
+
this.updater.clear();
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get the underlying updater for event handling.
|
|
245
|
+
*/
|
|
246
|
+
getUpdater() {
|
|
247
|
+
return this.updater;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Priority queue for updates - high priority items processed first.
|
|
252
|
+
*/
|
|
253
|
+
export class PriorityUpdater {
|
|
254
|
+
constructor(simile, config = {}) {
|
|
255
|
+
this.highPriority = [];
|
|
256
|
+
this.normalPriority = [];
|
|
257
|
+
this.processing = false;
|
|
258
|
+
this.timeoutId = null;
|
|
259
|
+
this.simile = simile;
|
|
260
|
+
this.config = {
|
|
261
|
+
batchDelay: config.batchDelay ?? 50,
|
|
262
|
+
maxBatchSize: config.maxBatchSize ?? 50,
|
|
263
|
+
...config,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Queue high priority item (processed first).
|
|
268
|
+
*/
|
|
269
|
+
queueHigh(items) {
|
|
270
|
+
this.highPriority.push(...items);
|
|
271
|
+
this.scheduleProcessing();
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Queue normal priority item.
|
|
275
|
+
*/
|
|
276
|
+
enqueue(items) {
|
|
277
|
+
this.normalPriority.push(...items);
|
|
278
|
+
this.scheduleProcessing();
|
|
279
|
+
}
|
|
280
|
+
scheduleProcessing() {
|
|
281
|
+
if (this.timeoutId || this.processing)
|
|
282
|
+
return;
|
|
283
|
+
this.timeoutId = setTimeout(() => {
|
|
284
|
+
this.timeoutId = null;
|
|
285
|
+
this.processQueue().catch(console.error);
|
|
286
|
+
}, this.config.batchDelay);
|
|
287
|
+
}
|
|
288
|
+
async processQueue() {
|
|
289
|
+
if (this.processing)
|
|
290
|
+
return;
|
|
291
|
+
this.processing = true;
|
|
292
|
+
const maxBatch = this.config.maxBatchSize ?? 50;
|
|
293
|
+
try {
|
|
294
|
+
// Process high priority first
|
|
295
|
+
while (this.highPriority.length > 0) {
|
|
296
|
+
const batch = this.highPriority.splice(0, maxBatch);
|
|
297
|
+
await this.simile.add(batch);
|
|
298
|
+
}
|
|
299
|
+
// Then normal priority
|
|
300
|
+
while (this.normalPriority.length > 0) {
|
|
301
|
+
const batch = this.normalPriority.splice(0, maxBatch);
|
|
302
|
+
await this.simile.add(batch);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
finally {
|
|
306
|
+
this.processing = false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Force immediate processing.
|
|
311
|
+
*/
|
|
312
|
+
async flush() {
|
|
313
|
+
if (this.timeoutId) {
|
|
314
|
+
clearTimeout(this.timeoutId);
|
|
315
|
+
this.timeoutId = null;
|
|
316
|
+
}
|
|
317
|
+
await this.processQueue();
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Get total pending count.
|
|
321
|
+
*/
|
|
322
|
+
getPendingCount() {
|
|
323
|
+
return this.highPriority.length + this.normalPriority.length;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Clear all pending items.
|
|
327
|
+
*/
|
|
328
|
+
clear() {
|
|
329
|
+
if (this.timeoutId) {
|
|
330
|
+
clearTimeout(this.timeoutId);
|
|
331
|
+
this.timeoutId = null;
|
|
332
|
+
}
|
|
333
|
+
this.highPriority = [];
|
|
334
|
+
this.normalPriority = [];
|
|
335
|
+
}
|
|
336
|
+
}
|