@feltdb/core 0.2.0 → 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 (95) hide show
  1. package/LICENSE +19 -0
  2. package/dist/analytics-backend.d.ts +39 -0
  3. package/dist/analytics-backend.d.ts.map +1 -0
  4. package/dist/analytics-backend.js +152 -0
  5. package/dist/application-contract.d.ts +49 -0
  6. package/dist/application-contract.d.ts.map +1 -0
  7. package/dist/application-contract.js +13 -0
  8. package/dist/application-manifest.d.ts +305 -0
  9. package/dist/application-manifest.d.ts.map +1 -0
  10. package/dist/application-manifest.js +26 -0
  11. package/dist/artifact.d.ts +73 -0
  12. package/dist/artifact.d.ts.map +1 -0
  13. package/dist/artifact.js +21 -0
  14. package/dist/authorization.d.ts +99 -0
  15. package/dist/authorization.d.ts.map +1 -0
  16. package/dist/authorization.js +15 -0
  17. package/dist/bundle.d.ts +55 -0
  18. package/dist/bundle.d.ts.map +1 -0
  19. package/dist/bundle.js +13 -0
  20. package/dist/collection.d.ts +28 -0
  21. package/dist/collection.d.ts.map +1 -1
  22. package/dist/collection.js +83 -0
  23. package/dist/db.d.ts +18 -0
  24. package/dist/db.d.ts.map +1 -1
  25. package/dist/db.js +20 -8
  26. package/dist/distributed-indexing.d.ts +170 -0
  27. package/dist/distributed-indexing.d.ts.map +1 -0
  28. package/dist/distributed-indexing.js +260 -0
  29. package/dist/flowspec.d.ts +4 -0
  30. package/dist/flowspec.d.ts.map +1 -1
  31. package/dist/flowspec.js +8 -0
  32. package/dist/http-client.d.ts +35 -0
  33. package/dist/http-client.d.ts.map +1 -0
  34. package/dist/http-client.js +70 -0
  35. package/dist/identity.d.ts +40 -0
  36. package/dist/identity.d.ts.map +1 -0
  37. package/dist/identity.js +10 -0
  38. package/dist/index-analytics.d.ts +121 -0
  39. package/dist/index-analytics.d.ts.map +1 -0
  40. package/dist/index-analytics.js +279 -0
  41. package/dist/index-backend.d.ts +26 -0
  42. package/dist/index-backend.d.ts.map +1 -0
  43. package/dist/index-backend.js +115 -0
  44. package/dist/index-dashboard.d.ts +114 -0
  45. package/dist/index-dashboard.d.ts.map +1 -0
  46. package/dist/index-dashboard.js +256 -0
  47. package/dist/index-manager.d.ts +18 -0
  48. package/dist/index-manager.d.ts.map +1 -0
  49. package/dist/index-manager.js +333 -0
  50. package/dist/index-monitoring.d.ts +133 -0
  51. package/dist/index-monitoring.d.ts.map +1 -0
  52. package/dist/index-monitoring.js +218 -0
  53. package/dist/index-recovery.d.ts +57 -0
  54. package/dist/index-recovery.d.ts.map +1 -0
  55. package/dist/index-recovery.js +117 -0
  56. package/dist/index-store.d.ts +62 -0
  57. package/dist/index-store.d.ts.map +1 -0
  58. package/dist/index-store.js +141 -0
  59. package/dist/index-types.d.ts +70 -0
  60. package/dist/index-types.d.ts.map +1 -0
  61. package/dist/index-types.js +6 -0
  62. package/dist/index.d.ts +26 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +26 -0
  65. package/dist/observe.d.ts +51 -0
  66. package/dist/observe.d.ts.map +1 -0
  67. package/dist/observe.js +14 -0
  68. package/dist/protocol.d.ts +25 -0
  69. package/dist/protocol.d.ts.map +1 -0
  70. package/dist/protocol.js +18 -0
  71. package/dist/provider.d.ts +61 -0
  72. package/dist/provider.d.ts.map +1 -0
  73. package/dist/provider.js +17 -0
  74. package/dist/query-planner.d.ts +69 -0
  75. package/dist/query-planner.d.ts.map +1 -0
  76. package/dist/query-planner.js +184 -0
  77. package/dist/release.d.ts +60 -0
  78. package/dist/release.d.ts.map +1 -0
  79. package/dist/release.js +18 -0
  80. package/dist/sharding.d.ts +159 -0
  81. package/dist/sharding.d.ts.map +1 -0
  82. package/dist/sharding.js +317 -0
  83. package/dist/state-contract.d.ts +173 -0
  84. package/dist/state-contract.d.ts.map +1 -0
  85. package/dist/state-contract.js +24 -0
  86. package/dist/sync-contract.d.ts +101 -0
  87. package/dist/sync-contract.d.ts.map +1 -0
  88. package/dist/sync-contract.js +90 -0
  89. package/dist/worker.d.ts +90 -0
  90. package/dist/worker.d.ts.map +1 -0
  91. package/dist/worker.js +26 -0
  92. package/dist/workload.d.ts +102 -0
  93. package/dist/workload.d.ts.map +1 -0
  94. package/dist/workload.js +23 -0
  95. package/package.json +17 -5
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Index Performance Dashboard
3
+ *
4
+ * Real-time visualization and monitoring of index performance metrics.
5
+ */
6
+ /**
7
+ * Dashboard formatter - converts raw metrics to display format.
8
+ */
9
+ export class DashboardFormatter {
10
+ /**
11
+ * Format index metrics for display.
12
+ */
13
+ static formatIndexMetrics(metrics) {
14
+ const now = Date.now();
15
+ const lastUsedMs = now - metrics.lastUsedAt;
16
+ const lastUsedRelative = this.formatDuration(lastUsedMs);
17
+ // Determine health status
18
+ let health = 'healthy';
19
+ let healthReason;
20
+ if (metrics.hitRate < 0.01 && lastUsedMs > 86400000) {
21
+ health = 'warning';
22
+ healthReason = 'Unused for 24+ hours';
23
+ }
24
+ else if (metrics.avgQueryTimeMs > 100) {
25
+ health = 'warning';
26
+ healthReason = 'Slow average query time';
27
+ }
28
+ else if (metrics.hitRate > 0.8 && metrics.reads > 1000) {
29
+ health = 'healthy';
30
+ healthReason = 'High-value index';
31
+ }
32
+ return {
33
+ name: metrics.name,
34
+ type: metrics.name.split('_').pop() || 'unknown',
35
+ reads: metrics.reads,
36
+ writes: metrics.writes,
37
+ hitRate: metrics.hitRate,
38
+ avgQueryMs: Math.round(metrics.avgQueryTimeMs * 100) / 100,
39
+ avgWriteMs: Math.round(metrics.avgWriteTimeMs * 100) / 100,
40
+ entries: metrics.entries,
41
+ lastUsedRelative,
42
+ health,
43
+ healthReason,
44
+ };
45
+ }
46
+ /**
47
+ * Format performance report for display.
48
+ */
49
+ static formatReport(report) {
50
+ const queryTraces = report.indexMetrics; // Simplified - would have actual traces
51
+ const indexUsageRate = report.indexedQueries / Math.max(report.totalQueries, 1);
52
+ // Calculate percentiles (simplified)
53
+ const avgQueryMs = report.avgQueryTimeMs;
54
+ const p50 = avgQueryMs;
55
+ const p95 = avgQueryMs * 1.5;
56
+ const p99 = avgQueryMs * 2;
57
+ return {
58
+ timestamp: report.timestamp,
59
+ indexes: report.indexMetrics.map(m => this.formatIndexMetrics(m)),
60
+ queryPerformance: {
61
+ totalQueries: report.totalQueries,
62
+ indexedQueries: report.indexedQueries,
63
+ fullScans: report.fullScans,
64
+ indexUsageRate,
65
+ avgQueryMs: Math.round(avgQueryMs * 100) / 100,
66
+ p50QueryMs: Math.round(p50 * 100) / 100,
67
+ p95QueryMs: Math.round(p95 * 100) / 100,
68
+ p99QueryMs: Math.round(p99 * 100) / 100,
69
+ slowQueries: Math.ceil(report.totalQueries * 0.05), // Estimate 5% slow
70
+ },
71
+ recommendations: report.recommendations,
72
+ };
73
+ }
74
+ /**
75
+ * Format duration for display.
76
+ */
77
+ static formatDuration(ms) {
78
+ if (ms < 1000)
79
+ return 'just now';
80
+ const seconds = Math.floor(ms / 1000);
81
+ if (seconds < 60)
82
+ return `${seconds} seconds ago`;
83
+ const minutes = Math.floor(seconds / 60);
84
+ if (minutes < 60)
85
+ return `${minutes} minutes ago`;
86
+ const hours = Math.floor(minutes / 60);
87
+ if (hours < 24)
88
+ return `${hours} hours ago`;
89
+ const days = Math.floor(hours / 24);
90
+ return `${days} days ago`;
91
+ }
92
+ /**
93
+ * Generate ASCII dashboard.
94
+ */
95
+ static renderASCII(dashboard) {
96
+ const lines = [];
97
+ lines.push('╔═══════════════════════════════════════════════════════════════╗');
98
+ lines.push('║ FeltDB Index Performance Dashboard ║');
99
+ lines.push('╠═══════════════════════════════════════════════════════════════╣');
100
+ // Query Performance Summary
101
+ const qp = dashboard.queryPerformance;
102
+ lines.push(`║ Queries: ${qp.totalQueries.toString().padEnd(10)} | Indexed: ${(qp.indexUsageRate * 100).toFixed(1)}% ${' '.repeat(10)} ║`);
103
+ lines.push(`║ Avg: ${qp.avgQueryMs.toFixed(2)}ms | P95: ${qp.p95QueryMs.toFixed(2)}ms | P99: ${qp.p99QueryMs.toFixed(2)}ms ${' '.repeat(5)} ║`);
104
+ lines.push('╟───────────────────────────────────────────────────────────────╢');
105
+ lines.push('║ Index Name │ Type │ Reads │ Hit │ Avg(ms) │ Health ║');
106
+ lines.push('╟─────────────────────┼─────────┼────────┼──────┼─────────┼────────╢');
107
+ for (const idx of dashboard.indexes) {
108
+ const health = idx.health === 'healthy' ? '✓' : idx.health === 'warning' ? '⚠' : '✗';
109
+ const line = `║ ${idx.name.padEnd(19)} │ ${idx.type.padEnd(7)} │ ${idx.reads.toString().padEnd(6)} │ ${(idx.hitRate * 100).toFixed(1).padEnd(4)}% │ ${idx.avgQueryMs.toFixed(3).padEnd(7)} │ ${health.padEnd(6)} ║`;
110
+ lines.push(line);
111
+ }
112
+ lines.push('╟───────────────────────────────────────────────────────────────╢');
113
+ lines.push('║ Recommendations ║');
114
+ lines.push('╟───────────────────────────────────────────────────────────────╢');
115
+ for (const rec of dashboard.recommendations.slice(0, 3)) {
116
+ const priority = rec.priority === 'high' ? '🔴' : rec.priority === 'medium' ? '🟡' : '🟢';
117
+ lines.push(`║ ${priority} ${rec.reason.substring(0, 57).padEnd(57)} ║`);
118
+ }
119
+ lines.push('╚═══════════════════════════════════════════════════════════════╝');
120
+ return lines.join('\n');
121
+ }
122
+ /**
123
+ * Generate JSON dashboard data.
124
+ */
125
+ static renderJSON(dashboard) {
126
+ return JSON.stringify(dashboard, null, 2);
127
+ }
128
+ }
129
+ /**
130
+ * Real-time dashboard updates via events.
131
+ */
132
+ export class LiveDashboard {
133
+ constructor() {
134
+ this.metrics = null;
135
+ this.listeners = new Set();
136
+ this.updateInterval = 10000; // 10 seconds
137
+ }
138
+ /**
139
+ * Start streaming dashboard updates.
140
+ */
141
+ start(updateFn, interval = 10000) {
142
+ this.updateInterval = interval;
143
+ this.intervalHandle = setInterval(() => {
144
+ this.metrics = updateFn();
145
+ this.notifyListeners();
146
+ }, this.updateInterval);
147
+ }
148
+ /**
149
+ * Stop streaming updates.
150
+ */
151
+ stop() {
152
+ if (this.intervalHandle) {
153
+ clearInterval(this.intervalHandle);
154
+ this.intervalHandle = undefined;
155
+ }
156
+ }
157
+ /**
158
+ * Subscribe to dashboard updates.
159
+ */
160
+ subscribe(listener) {
161
+ this.listeners.add(listener);
162
+ if (this.metrics) {
163
+ listener(this.metrics);
164
+ }
165
+ return () => {
166
+ this.listeners.delete(listener);
167
+ };
168
+ }
169
+ /**
170
+ * Get current dashboard state.
171
+ */
172
+ getCurrent() {
173
+ return this.metrics;
174
+ }
175
+ notifyListeners() {
176
+ if (!this.metrics)
177
+ return;
178
+ for (const listener of this.listeners) {
179
+ listener(this.metrics);
180
+ }
181
+ }
182
+ }
183
+ export class PerformanceAlerter {
184
+ constructor() {
185
+ this.alerts = [];
186
+ this.thresholds = {
187
+ slowQueryMs: 100,
188
+ fullScanThreshold: 0.2, // 20% full scans is warning
189
+ indexOverhead: 0.1, // 10% write overhead is warning
190
+ };
191
+ }
192
+ /**
193
+ * Check dashboard against thresholds and generate alerts.
194
+ */
195
+ checkThresholds(dashboard) {
196
+ const newAlerts = [];
197
+ const qp = dashboard.queryPerformance;
198
+ // Alert on slow average query time
199
+ if (qp.avgQueryMs > this.thresholds.slowQueryMs) {
200
+ newAlerts.push({
201
+ severity: 'warning',
202
+ type: 'slow_query',
203
+ message: `Average query time ${qp.avgQueryMs.toFixed(2)}ms exceeds threshold`,
204
+ timestamp: Date.now(),
205
+ metrics: {
206
+ avgQueryMs: qp.avgQueryMs,
207
+ threshold: this.thresholds.slowQueryMs,
208
+ },
209
+ });
210
+ }
211
+ // Alert on high full scan rate
212
+ const fullScanRate = qp.fullScans / qp.totalQueries;
213
+ if (fullScanRate > this.thresholds.fullScanThreshold) {
214
+ newAlerts.push({
215
+ severity: 'warning',
216
+ type: 'high_full_scans',
217
+ message: `Full scans at ${(fullScanRate * 100).toFixed(1)}% exceeds threshold`,
218
+ timestamp: Date.now(),
219
+ metrics: {
220
+ fullScanRate: (fullScanRate * 100).toFixed(1),
221
+ threshold: this.thresholds.fullScanThreshold * 100,
222
+ },
223
+ });
224
+ }
225
+ // Alert on unused indexes
226
+ for (const idx of dashboard.indexes) {
227
+ if (idx.hitRate < 0.01 && idx.lastUsedRelative.includes('day')) {
228
+ newAlerts.push({
229
+ severity: 'info',
230
+ type: 'unused_index',
231
+ message: `Index '${idx.name}' has low hit rate and is unused`,
232
+ timestamp: Date.now(),
233
+ metrics: {
234
+ index: idx.name,
235
+ hitRate: (idx.hitRate * 100).toFixed(2),
236
+ },
237
+ });
238
+ }
239
+ }
240
+ this.alerts.push(...newAlerts);
241
+ return newAlerts;
242
+ }
243
+ /**
244
+ * Get recent alerts.
245
+ */
246
+ getRecentAlerts(limit = 50) {
247
+ return this.alerts.slice(-limit);
248
+ }
249
+ /**
250
+ * Clear old alerts.
251
+ */
252
+ clearOldAlerts(olderThanMs = 3600000) {
253
+ const cutoff = Date.now() - olderThanMs;
254
+ this.alerts = this.alerts.filter(a => a.timestamp > cutoff);
255
+ }
256
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Index Manager
3
+ *
4
+ * Manages creation, maintenance, and query optimization using indexes.
5
+ */
6
+ import type { IndexConfig, IndexQuery, IndexStats, QueryPlan } from './index-types.js';
7
+ export declare class IndexManager {
8
+ private indexes;
9
+ private config;
10
+ private indexStats;
11
+ createIndex(config: IndexConfig): void;
12
+ updateRecord(recordId: string, oldValue: Record<string, any> | null, newValue: Record<string, any>): void;
13
+ queryIndex(name: string, query: IndexQuery): string[];
14
+ listIndexes(): IndexConfig[];
15
+ getStats(name: string): IndexStats | null;
16
+ planQuery(fields: string[]): QueryPlan;
17
+ }
18
+ //# sourceMappingURL=index-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index-manager.d.ts","sourceRoot":"","sources":["../src/index-manager.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAc,UAAU,EAAE,UAAU,EAAe,SAAS,EAAiB,MAAM,kBAAkB,CAAC;AAyN/H,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAyE;IACxF,OAAO,CAAC,MAAM,CAAuC;IACrD,OAAO,CAAC,UAAU,CAAsC;IAExD,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IA8BtC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IA6BzG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE;IA0BrD,WAAW,IAAI,WAAW,EAAE;IAI5B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAIzC,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,SAAS;CAwCvC"}
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Index Manager
3
+ *
4
+ * Manages creation, maintenance, and query optimization using indexes.
5
+ */
6
+ class HashIndex {
7
+ constructor(field) {
8
+ this.entries = new Map();
9
+ this.stats = { reads: 0, writes: 0, selectivity: 0 };
10
+ this.field = field;
11
+ }
12
+ add(recordId, value) {
13
+ const key = this.normalizeKey(value);
14
+ if (!this.entries.has(key)) {
15
+ this.entries.set(key, new Set());
16
+ }
17
+ this.entries.get(key).add(recordId);
18
+ this.stats.writes++;
19
+ }
20
+ remove(recordId, value) {
21
+ const key = this.normalizeKey(value);
22
+ const ids = this.entries.get(key);
23
+ if (ids) {
24
+ ids.delete(recordId);
25
+ if (ids.size === 0) {
26
+ this.entries.delete(key);
27
+ }
28
+ }
29
+ this.stats.writes++;
30
+ }
31
+ query(value) {
32
+ const key = this.normalizeKey(value);
33
+ this.stats.reads++;
34
+ const ids = this.entries.get(key);
35
+ return ids ? Array.from(ids) : [];
36
+ }
37
+ size() {
38
+ return this.entries.size;
39
+ }
40
+ getStats() {
41
+ return {
42
+ entries: this.entries.size,
43
+ reads: this.stats.reads,
44
+ writes: this.stats.writes,
45
+ selectivity: this.entries.size > 0 ? 1 / this.entries.size : 0,
46
+ };
47
+ }
48
+ normalizeKey(value) {
49
+ if (value === null || value === undefined)
50
+ return '__null__';
51
+ if (typeof value === 'object')
52
+ return JSON.stringify(value);
53
+ return String(value);
54
+ }
55
+ }
56
+ class SortedIndex {
57
+ constructor(field) {
58
+ this.entries = [];
59
+ this.stats = { reads: 0, writes: 0, selectivity: 0 };
60
+ this.field = field;
61
+ }
62
+ add(recordId, value) {
63
+ let entry = this.entries.find(e => this.compareKeys(e.key, value) === 0);
64
+ if (!entry) {
65
+ entry = { key: value, recordIds: new Set() };
66
+ this.entries.push(entry);
67
+ this.entries.sort((a, b) => this.compareKeys(a.key, b.key));
68
+ }
69
+ entry.recordIds.add(recordId);
70
+ this.stats.writes++;
71
+ }
72
+ remove(recordId, value) {
73
+ const entry = this.entries.find(e => this.compareKeys(e.key, value) === 0);
74
+ if (entry) {
75
+ entry.recordIds.delete(recordId);
76
+ if (entry.recordIds.size === 0) {
77
+ this.entries = this.entries.filter(e => e !== entry);
78
+ }
79
+ }
80
+ this.stats.writes++;
81
+ }
82
+ queryEqual(value) {
83
+ this.stats.reads++;
84
+ const entry = this.entries.find(e => this.compareKeys(e.key, value) === 0);
85
+ return entry ? Array.from(entry.recordIds) : [];
86
+ }
87
+ queryRange(min, max, includeMin = true, includeMax = true) {
88
+ this.stats.reads++;
89
+ const result = new Set();
90
+ for (const entry of this.entries) {
91
+ const cmpMin = this.compareKeys(entry.key, min);
92
+ const cmpMax = this.compareKeys(entry.key, max);
93
+ const inRange = (includeMin ? cmpMin >= 0 : cmpMin > 0) && (includeMax ? cmpMax <= 0 : cmpMax < 0);
94
+ if (inRange) {
95
+ entry.recordIds.forEach(id => result.add(id));
96
+ }
97
+ }
98
+ return Array.from(result);
99
+ }
100
+ size() {
101
+ return this.entries.length;
102
+ }
103
+ getStats() {
104
+ const totalIds = this.entries.reduce((sum, e) => sum + e.recordIds.size, 0);
105
+ return {
106
+ entries: this.entries.length,
107
+ reads: this.stats.reads,
108
+ writes: this.stats.writes,
109
+ selectivity: this.entries.length > 0 ? 1 / this.entries.length : 0,
110
+ };
111
+ }
112
+ compareKeys(a, b) {
113
+ if (a === null || a === undefined)
114
+ return -1;
115
+ if (b === null || b === undefined)
116
+ return 1;
117
+ if (a < b)
118
+ return -1;
119
+ if (a > b)
120
+ return 1;
121
+ return 0;
122
+ }
123
+ }
124
+ class TextIndex {
125
+ constructor(fields) {
126
+ this.tokenIndex = new Map();
127
+ this.stats = { reads: 0, writes: 0, selectivity: 0 };
128
+ this.fields = fields;
129
+ }
130
+ add(recordId, data) {
131
+ const text = this.extractText(data);
132
+ const tokens = this.tokenize(text);
133
+ for (const token of tokens) {
134
+ if (!this.tokenIndex.has(token)) {
135
+ this.tokenIndex.set(token, new Set());
136
+ }
137
+ this.tokenIndex.get(token).add(recordId);
138
+ }
139
+ this.stats.writes++;
140
+ }
141
+ remove(recordId, data) {
142
+ const text = this.extractText(data);
143
+ const tokens = this.tokenize(text);
144
+ for (const token of tokens) {
145
+ const ids = this.tokenIndex.get(token);
146
+ if (ids) {
147
+ ids.delete(recordId);
148
+ if (ids.size === 0) {
149
+ this.tokenIndex.delete(token);
150
+ }
151
+ }
152
+ }
153
+ this.stats.writes++;
154
+ }
155
+ query(searchText) {
156
+ this.stats.reads++;
157
+ const tokens = this.tokenize(searchText);
158
+ if (tokens.length === 0)
159
+ return [];
160
+ let result = this.tokenIndex.get(tokens[0]);
161
+ if (!result)
162
+ return [];
163
+ result = new Set(result);
164
+ for (let i = 1; i < tokens.length; i++) {
165
+ const tokenIds = this.tokenIndex.get(tokens[i]);
166
+ if (!tokenIds)
167
+ return [];
168
+ result = new Set([...result].filter(id => tokenIds.has(id)));
169
+ }
170
+ return Array.from(result);
171
+ }
172
+ size() {
173
+ return this.tokenIndex.size;
174
+ }
175
+ getStats() {
176
+ return {
177
+ entries: this.tokenIndex.size,
178
+ reads: this.stats.reads,
179
+ writes: this.stats.writes,
180
+ selectivity: this.tokenIndex.size > 0 ? 1 / this.tokenIndex.size : 0,
181
+ };
182
+ }
183
+ extractText(data) {
184
+ return this.fields
185
+ .map(field => {
186
+ const value = data[field];
187
+ return value ? String(value) : '';
188
+ })
189
+ .join(' ');
190
+ }
191
+ tokenize(text) {
192
+ return text
193
+ .toLowerCase()
194
+ .split(/\s+/)
195
+ .filter(t => t.length > 0);
196
+ }
197
+ }
198
+ export class IndexManager {
199
+ constructor() {
200
+ this.indexes = new Map();
201
+ this.config = new Map();
202
+ this.indexStats = new Map();
203
+ }
204
+ createIndex(config) {
205
+ if (this.indexes.has(config.name)) {
206
+ throw new Error(`Index ${config.name} already exists`);
207
+ }
208
+ let index;
209
+ if (config.type === 'hash' && config.field) {
210
+ index = new HashIndex(config.field);
211
+ }
212
+ else if (config.type === 'sorted' && config.field) {
213
+ index = new SortedIndex(config.field);
214
+ }
215
+ else if (config.type === 'text' && config.textFields) {
216
+ index = new TextIndex(config.textFields);
217
+ }
218
+ else {
219
+ throw new Error(`Invalid index config: ${JSON.stringify(config)}`);
220
+ }
221
+ this.indexes.set(config.name, index);
222
+ this.config.set(config.name, config);
223
+ this.indexStats.set(config.name, {
224
+ name: config.name,
225
+ type: config.type,
226
+ entries: 0,
227
+ size: 0,
228
+ lastUpdated: Date.now(),
229
+ reads: 0,
230
+ writes: 0,
231
+ selectivity: 0,
232
+ });
233
+ }
234
+ updateRecord(recordId, oldValue, newValue) {
235
+ for (const [name, index] of this.indexes) {
236
+ const config = this.config.get(name);
237
+ if (oldValue) {
238
+ if (config.type === 'hash' && config.field) {
239
+ index.remove(recordId, oldValue[config.field]);
240
+ }
241
+ else if (config.type === 'sorted' && config.field) {
242
+ index.remove(recordId, oldValue[config.field]);
243
+ }
244
+ else if (config.type === 'text') {
245
+ index.remove(recordId, oldValue);
246
+ }
247
+ }
248
+ if (config.type === 'hash' && config.field) {
249
+ index.add(recordId, newValue[config.field]);
250
+ }
251
+ else if (config.type === 'sorted' && config.field) {
252
+ index.add(recordId, newValue[config.field]);
253
+ }
254
+ else if (config.type === 'text') {
255
+ index.add(recordId, newValue);
256
+ }
257
+ const stats = this.indexStats.get(name);
258
+ stats.lastUpdated = Date.now();
259
+ const indexStats = index.getStats();
260
+ Object.assign(stats, indexStats);
261
+ }
262
+ }
263
+ queryIndex(name, query) {
264
+ const index = this.indexes.get(name);
265
+ if (!index)
266
+ throw new Error(`Index ${name} not found`);
267
+ const config = this.config.get(name);
268
+ if (config.type === 'hash') {
269
+ return index.query(query.value);
270
+ }
271
+ else if (config.type === 'sorted') {
272
+ const sorted = index;
273
+ if (query.operator === 'eq') {
274
+ return sorted.queryEqual(query.value);
275
+ }
276
+ else if (query.operator === 'range') {
277
+ return sorted.queryRange(query.min, query.max);
278
+ }
279
+ else if (query.operator === 'gte') {
280
+ return sorted.queryRange(query.value, Number.MAX_VALUE);
281
+ }
282
+ else if (query.operator === 'lte') {
283
+ return sorted.queryRange(Number.MIN_VALUE, query.value);
284
+ }
285
+ }
286
+ else if (config.type === 'text') {
287
+ return index.query(query.value);
288
+ }
289
+ return [];
290
+ }
291
+ listIndexes() {
292
+ return Array.from(this.config.values());
293
+ }
294
+ getStats(name) {
295
+ return this.indexStats.get(name) || null;
296
+ }
297
+ planQuery(fields) {
298
+ const plans = [];
299
+ for (const field of fields) {
300
+ for (const [name, config] of this.config) {
301
+ if (config.field === field || config.fields?.includes(field)) {
302
+ const stats = this.indexStats.get(name);
303
+ plans.push({
304
+ indexName: name,
305
+ indexType: config.type,
306
+ estimatedRows: Math.ceil(100 / (stats.selectivity || 1)),
307
+ cost: stats.reads + stats.writes,
308
+ canPruneAll: config.type === 'hash',
309
+ requiresPostFilter: config.type === 'text' || config.type === 'sorted',
310
+ });
311
+ }
312
+ }
313
+ }
314
+ if (plans.length === 0) {
315
+ return {
316
+ strategy: 'full-scan',
317
+ indexes: [],
318
+ estimatedRows: 1000,
319
+ totalCost: 1000,
320
+ explanation: 'No suitable index found, using full collection scan',
321
+ };
322
+ }
323
+ plans.sort((a, b) => a.cost - b.cost);
324
+ const best = plans[0];
325
+ return {
326
+ strategy: 'index-scan',
327
+ indexes: [best],
328
+ estimatedRows: best.estimatedRows,
329
+ totalCost: best.cost,
330
+ explanation: `Using ${best.indexType} index on ${best.indexName}`,
331
+ };
332
+ }
333
+ }