@vue-skuilder/db 0.1.4 → 0.1.6

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 (83) hide show
  1. package/CLAUDE.md +43 -0
  2. package/dist/SyncStrategy-DnJRj-Xp.d.mts +74 -0
  3. package/dist/SyncStrategy-DnJRj-Xp.d.ts +74 -0
  4. package/dist/core/index.d.mts +90 -2
  5. package/dist/core/index.d.ts +90 -2
  6. package/dist/core/index.js +856 -6155
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/index.mjs +778 -6097
  9. package/dist/core/index.mjs.map +1 -1
  10. package/dist/dataLayerProvider-BZmLyBVw.d.mts +41 -0
  11. package/dist/dataLayerProvider-BuntXkCs.d.ts +41 -0
  12. package/dist/impl/couch/index.d.mts +292 -0
  13. package/dist/impl/couch/index.d.ts +292 -0
  14. package/dist/impl/couch/index.js +3075 -0
  15. package/dist/impl/couch/index.js.map +1 -0
  16. package/dist/impl/couch/index.mjs +3007 -0
  17. package/dist/impl/couch/index.mjs.map +1 -0
  18. package/dist/impl/static/index.d.mts +188 -0
  19. package/dist/impl/static/index.d.ts +188 -0
  20. package/dist/impl/static/index.js +3055 -0
  21. package/dist/impl/static/index.js.map +1 -0
  22. package/dist/impl/static/index.mjs +3025 -0
  23. package/dist/impl/static/index.mjs.map +1 -0
  24. package/dist/index.d.mts +13 -4
  25. package/dist/index.d.ts +13 -4
  26. package/dist/index.js +2920 -6846
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +3567 -7513
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/types-D6SnlHPm.d.ts +58 -0
  31. package/dist/types-DPRvCrIk.d.mts +58 -0
  32. package/dist/types-legacy-WPe8CtO-.d.mts +139 -0
  33. package/dist/types-legacy-WPe8CtO-.d.ts +139 -0
  34. package/dist/{index-QMtzQI65.d.mts → userDB-31gsvxyd.d.mts} +11 -252
  35. package/dist/{index-QMtzQI65.d.ts → userDB-D9EuWTp1.d.ts} +11 -252
  36. package/dist/util/packer/index.d.mts +65 -0
  37. package/dist/util/packer/index.d.ts +65 -0
  38. package/dist/util/packer/index.js +512 -0
  39. package/dist/util/packer/index.js.map +1 -0
  40. package/dist/util/packer/index.mjs +485 -0
  41. package/dist/util/packer/index.mjs.map +1 -0
  42. package/package.json +12 -2
  43. package/src/core/interfaces/contentSource.ts +8 -6
  44. package/src/core/interfaces/courseDB.ts +1 -1
  45. package/src/core/interfaces/dataLayerProvider.ts +5 -0
  46. package/src/core/interfaces/userDB.ts +7 -2
  47. package/src/core/types/types-legacy.ts +2 -0
  48. package/src/factory.ts +10 -7
  49. package/src/impl/{pouch/userDB.ts → common/BaseUserDB.ts} +283 -260
  50. package/src/impl/common/SyncStrategy.ts +90 -0
  51. package/src/impl/common/index.ts +23 -0
  52. package/src/impl/common/types.ts +50 -0
  53. package/src/impl/common/userDBHelpers.ts +144 -0
  54. package/src/impl/couch/CouchDBSyncStrategy.ts +209 -0
  55. package/src/impl/{pouch → couch}/PouchDataLayerProvider.ts +16 -7
  56. package/src/impl/{pouch → couch}/adminDB.ts +3 -3
  57. package/src/impl/{pouch → couch}/auth.ts +2 -2
  58. package/src/impl/{pouch → couch}/classroomDB.ts +6 -6
  59. package/src/impl/{pouch → couch}/courseAPI.ts +59 -21
  60. package/src/impl/{pouch → couch}/courseDB.ts +32 -17
  61. package/src/impl/{pouch → couch}/courseLookupDB.ts +1 -1
  62. package/src/impl/{pouch → couch}/index.ts +27 -20
  63. package/src/impl/{pouch → couch}/updateQueue.ts +5 -1
  64. package/src/impl/{pouch → couch}/user-course-relDB.ts +6 -1
  65. package/src/impl/static/NoOpSyncStrategy.ts +70 -0
  66. package/src/impl/static/StaticDataLayerProvider.ts +93 -0
  67. package/src/impl/static/StaticDataUnpacker.ts +549 -0
  68. package/src/impl/static/courseDB.ts +275 -0
  69. package/src/impl/static/coursesDB.ts +37 -0
  70. package/src/impl/static/index.ts +7 -0
  71. package/src/index.ts +1 -1
  72. package/src/study/SessionController.ts +4 -4
  73. package/src/study/SpacedRepetition.ts +3 -3
  74. package/src/study/getCardDataShape.ts +2 -2
  75. package/src/util/index.ts +1 -0
  76. package/src/util/packer/CouchDBToStaticPacker.ts +620 -0
  77. package/src/util/packer/index.ts +4 -0
  78. package/src/util/packer/types.ts +64 -0
  79. package/tsconfig.json +7 -10
  80. package/tsup.config.ts +5 -3
  81. /package/src/impl/{pouch → couch}/clientCache.ts +0 -0
  82. /package/src/impl/{pouch → couch}/pouchdb-setup.ts +0 -0
  83. /package/src/impl/{pouch → couch}/types.ts +0 -0
@@ -0,0 +1,549 @@
1
+ // packages/db/src/impl/static/StaticDataUnpacker.ts
2
+
3
+ import { StaticCourseManifest, ChunkMetadata } from '../../util/packer/types';
4
+ import { logger } from '../../util/logger';
5
+ import { DocType } from '@db/core';
6
+
7
+ // Browser-compatible path utilities
8
+ const pathUtils = {
9
+ isAbsolute: (path: string): boolean => {
10
+ // Check for Windows absolute paths (C:\ or \\server\)
11
+ if (/^[a-zA-Z]:[\\/]/.test(path) || /^\\\\/.test(path)) {
12
+ return true;
13
+ }
14
+ // Check for Unix absolute paths (/)
15
+ if (path.startsWith('/')) {
16
+ return true;
17
+ }
18
+ return false;
19
+ },
20
+ };
21
+
22
+ // Check if we're in Node.js environment and fs is available
23
+ let nodeFS: any = null;
24
+ try {
25
+ // Use eval to prevent bundlers from including fs in browser builds
26
+ if (typeof window === 'undefined' && typeof process !== 'undefined' && process.versions?.node) {
27
+ nodeFS = eval('require')('fs');
28
+ }
29
+ } catch {
30
+ // fs not available, will use fetch
31
+ }
32
+
33
+ interface EloIndexEntry {
34
+ elo: number;
35
+ cardId: string;
36
+ }
37
+
38
+ interface EloIndex {
39
+ sorted: EloIndexEntry[];
40
+ buckets: Record<string, EloIndexEntry[]>;
41
+ stats: {
42
+ min: number;
43
+ max: number;
44
+ count: number;
45
+ };
46
+ }
47
+
48
+ interface TagsIndex {
49
+ byTag: Record<string, string[]>; // tagName -> cardIds
50
+ byCard: Record<string, string[]>; // cardId -> tagNames
51
+ }
52
+
53
+ export class StaticDataUnpacker {
54
+ private manifest: StaticCourseManifest;
55
+ private basePath: string;
56
+ private documentCache: Map<string, any> = new Map();
57
+ private chunkCache: Map<string, any[]> = new Map();
58
+ private indexCache: Map<string, any> = new Map();
59
+
60
+ constructor(manifest: StaticCourseManifest, basePath: string) {
61
+ this.manifest = manifest;
62
+ this.basePath = basePath;
63
+ }
64
+
65
+ /**
66
+ * Get a document by ID, loading from appropriate chunk if needed
67
+ */
68
+ async getDocument<T = any>(id: string): Promise<T> {
69
+ // Check document cache first
70
+ if (this.documentCache.has(id)) {
71
+ const doc = this.documentCache.get(id);
72
+ return await this.hydrateAttachments(doc);
73
+ }
74
+
75
+ // Find which chunk contains this document
76
+ const chunk = await this.findChunkForDocument(id);
77
+ if (!chunk) {
78
+ logger.error(
79
+ `Document ${id} not found in any chunk. Available chunks:`,
80
+ this.manifest.chunks.map((c) => `${c.id} (${c.docType}): ${c.startKey} - ${c.endKey}`)
81
+ );
82
+ throw new Error(`Document ${id} not found in any chunk`);
83
+ }
84
+
85
+ // Load the chunk if not cached
86
+ await this.loadChunk(chunk.id);
87
+
88
+ // Try to get the document from cache again
89
+ if (this.documentCache.has(id)) {
90
+ const doc = this.documentCache.get(id);
91
+ return await this.hydrateAttachments(doc);
92
+ }
93
+
94
+ logger.error(`Document ${id} not found in chunk ${chunk.id}`);
95
+ throw new Error(`Document ${id} not found in chunk ${chunk.id}`);
96
+ }
97
+
98
+ /**
99
+ * Query cards by ELO score, returning card IDs sorted by ELO
100
+ */
101
+ async queryByElo(targetElo: number, limit: number = 25): Promise<string[]> {
102
+ const eloIndex = (await this.loadIndex('elo')) as EloIndex;
103
+
104
+ if (!eloIndex || !eloIndex.sorted) {
105
+ logger.warn('ELO index not found or malformed, returning empty results');
106
+ return [];
107
+ }
108
+
109
+ // Find cards near the target ELO
110
+ const sorted = eloIndex.sorted;
111
+ let startIndex = 0;
112
+
113
+ // Binary search to find insertion point for target ELO
114
+ let left = 0;
115
+ let right = sorted.length - 1;
116
+ while (left <= right) {
117
+ const mid = Math.floor((left + right) / 2);
118
+ if (sorted[mid].elo < targetElo) {
119
+ left = mid + 1;
120
+ } else {
121
+ right = mid - 1;
122
+ }
123
+ }
124
+ startIndex = left;
125
+
126
+ // Collect cards around the target ELO
127
+ const result: string[] = [];
128
+ const halfLimit = Math.floor(limit / 2);
129
+
130
+ // Get cards below target ELO
131
+ for (
132
+ let i = Math.max(0, startIndex - halfLimit);
133
+ i < startIndex && result.length < limit;
134
+ i++
135
+ ) {
136
+ result.push(sorted[i].cardId);
137
+ }
138
+
139
+ // Get cards at or above target ELO
140
+ for (let i = startIndex; i < sorted.length && result.length < limit; i++) {
141
+ result.push(sorted[i].cardId);
142
+ }
143
+
144
+ return result;
145
+ }
146
+
147
+ /**
148
+ * Get all tag names mapped to their card arrays
149
+ */
150
+ async getTagsIndex(): Promise<TagsIndex> {
151
+ return (await this.loadIndex('tags')) as TagsIndex;
152
+ }
153
+
154
+ /**
155
+ * Find which chunk contains a specific document ID
156
+ */
157
+ private async findChunkForDocument(docId: string): Promise<ChunkMetadata | undefined> {
158
+ // Determine document type from ID pattern by checking all DocType enum members
159
+ let expectedDocType: DocType | undefined = undefined;
160
+
161
+ // Check for ID prefixes matching any DocType enum value
162
+ for (const docType of Object.values(DocType)) {
163
+ if (docId.startsWith(`${docType}-`)) {
164
+ expectedDocType = docType;
165
+ break;
166
+ }
167
+ }
168
+
169
+ if (expectedDocType !== undefined) {
170
+ // Use chunk filtering by docType for documents with recognized prefixes
171
+ const typeChunks = this.manifest.chunks.filter((c) => c.docType === expectedDocType);
172
+
173
+ for (const chunk of typeChunks) {
174
+ if (docId >= chunk.startKey && docId <= chunk.endKey) {
175
+ // Verify document actually exists in chunk
176
+ const exists = await this.verifyDocumentInChunk(docId, chunk);
177
+ if (exists) {
178
+ return chunk;
179
+ }
180
+ }
181
+ }
182
+
183
+ return undefined;
184
+ } else {
185
+ // Fall back to trying all chunk types with strict verification
186
+ // Since card IDs and displayable data IDs can overlap in range, we need to verify actual existence
187
+
188
+ // First try DISPLAYABLE_DATA chunks (most likely for documents without prefixes)
189
+ const displayableChunks = this.manifest.chunks.filter(
190
+ (c) => c.docType === 'DISPLAYABLE_DATA'
191
+ );
192
+ for (const chunk of displayableChunks) {
193
+ if (docId >= chunk.startKey && docId <= chunk.endKey) {
194
+ // Verify document actually exists in chunk
195
+ const exists = await this.verifyDocumentInChunk(docId, chunk);
196
+ if (exists) {
197
+ return chunk;
198
+ }
199
+ }
200
+ }
201
+
202
+ // Then try CARD chunks (for legacy card IDs without prefixes)
203
+ const cardChunks = this.manifest.chunks.filter((c) => c.docType === 'CARD');
204
+ for (const chunk of cardChunks) {
205
+ if (docId >= chunk.startKey && docId <= chunk.endKey) {
206
+ // Verify document actually exists in chunk
207
+ const exists = await this.verifyDocumentInChunk(docId, chunk);
208
+ if (exists) {
209
+ return chunk;
210
+ }
211
+ }
212
+ }
213
+
214
+ // Finally try any other chunk types
215
+ const otherChunks = this.manifest.chunks.filter(
216
+ (c) => c.docType !== 'CARD' && c.docType !== 'DISPLAYABLE_DATA' && c.docType !== 'TAG'
217
+ );
218
+ for (const chunk of otherChunks) {
219
+ if (docId >= chunk.startKey && docId <= chunk.endKey) {
220
+ // Verify document actually exists in chunk
221
+ const exists = await this.verifyDocumentInChunk(docId, chunk);
222
+ if (exists) {
223
+ return chunk;
224
+ }
225
+ }
226
+ }
227
+
228
+ return undefined;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Verify that a document actually exists in a specific chunk by loading and checking it
234
+ */
235
+ private async verifyDocumentInChunk(docId: string, chunk: ChunkMetadata): Promise<boolean> {
236
+ try {
237
+ // Load the chunk if not already cached
238
+ await this.loadChunk(chunk.id);
239
+
240
+ // Check if the document is now in our document cache
241
+ return this.documentCache.has(docId);
242
+ } catch {
243
+ return false;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Load a chunk file and cache its documents
249
+ */
250
+ private async loadChunk(chunkId: string): Promise<void> {
251
+ if (this.chunkCache.has(chunkId)) {
252
+ return; // Already loaded
253
+ }
254
+
255
+ const chunk = this.manifest.chunks.find((c) => c.id === chunkId);
256
+ if (!chunk) {
257
+ throw new Error(`Chunk ${chunkId} not found in manifest`);
258
+ }
259
+
260
+ try {
261
+ const chunkPath = `${this.basePath}/${chunk.path}`;
262
+ logger.debug(`Loading chunk from ${chunkPath}`);
263
+
264
+ let documents: any[];
265
+
266
+ // Check if we're in a Node.js environment with local files
267
+ if (this.isLocalPath(chunkPath) && nodeFS) {
268
+ // Use fs for local file access (e.g., in tests)
269
+ const fileContent = await nodeFS.promises.readFile(chunkPath, 'utf8');
270
+ documents = JSON.parse(fileContent);
271
+ } else {
272
+ // Use fetch for URL-based access (e.g., in browser)
273
+ const response = await fetch(chunkPath);
274
+ if (!response.ok) {
275
+ throw new Error(
276
+ `Failed to fetch chunk ${chunkId}: ${response.status} ${response.statusText}`
277
+ );
278
+ }
279
+ documents = await response.json();
280
+ }
281
+
282
+ this.chunkCache.set(chunkId, documents);
283
+
284
+ // Cache individual documents for quick lookup
285
+ for (const doc of documents) {
286
+ if (doc._id) {
287
+ this.documentCache.set(doc._id, doc);
288
+ }
289
+ }
290
+
291
+ logger.debug(`Loaded ${documents.length} documents from chunk ${chunkId}`);
292
+ } catch (error) {
293
+ logger.error(`Failed to load chunk ${chunkId}:`, error);
294
+ throw error;
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Load an index file and cache it
300
+ */
301
+ private async loadIndex(indexName: string): Promise<any> {
302
+ if (this.indexCache.has(indexName)) {
303
+ return this.indexCache.get(indexName);
304
+ }
305
+
306
+ const indexMeta = this.manifest.indices.find((idx) => idx.name === indexName);
307
+ if (!indexMeta) {
308
+ throw new Error(`Index ${indexName} not found in manifest`);
309
+ }
310
+
311
+ try {
312
+ const indexPath = `${this.basePath}/${indexMeta.path}`;
313
+ logger.debug(`Loading index from ${indexPath}`);
314
+
315
+ let indexData: any;
316
+
317
+ // Check if we're in a Node.js environment with local files
318
+ if (this.isLocalPath(indexPath) && nodeFS) {
319
+ // Use fs for local file access (e.g., in tests)
320
+ const fileContent = await nodeFS.promises.readFile(indexPath, 'utf8');
321
+ indexData = JSON.parse(fileContent);
322
+ } else {
323
+ // Use fetch for URL-based access (e.g., in browser)
324
+ const response = await fetch(indexPath);
325
+ if (!response.ok) {
326
+ throw new Error(
327
+ `Failed to fetch index ${indexName}: ${response.status} ${response.statusText}`
328
+ );
329
+ }
330
+ indexData = await response.json();
331
+ }
332
+
333
+ this.indexCache.set(indexName, indexData);
334
+
335
+ logger.debug(`Loaded index ${indexName}`);
336
+ return indexData;
337
+ } catch (error) {
338
+ logger.error(`Failed to load index ${indexName}:`, error);
339
+ throw error;
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Get a document by ID without hydration (raw document access)
345
+ * Used internally to avoid recursion during attachment hydration
346
+ */
347
+ private async getRawDocument<T = any>(id: string): Promise<T> {
348
+ // Check document cache first
349
+ if (this.documentCache.has(id)) {
350
+ return this.documentCache.get(id);
351
+ }
352
+
353
+ // Find which chunk contains this document
354
+ const chunk = await this.findChunkForDocument(id);
355
+ if (!chunk) {
356
+ logger.error(
357
+ `Document ${id} not found in any chunk. Available chunks:`,
358
+ this.manifest.chunks.map((c) => `${c.id} (${c.docType}): ${c.startKey} - ${c.endKey}`)
359
+ );
360
+ throw new Error(`Document ${id} not found in any chunk`);
361
+ }
362
+
363
+ // Load the chunk if not cached
364
+ await this.loadChunk(chunk.id);
365
+
366
+ // Try to get the document from cache again
367
+ if (this.documentCache.has(id)) {
368
+ return this.documentCache.get(id);
369
+ }
370
+
371
+ logger.error(`Document ${id} not found in chunk ${chunk.id}`);
372
+ throw new Error(`Document ${id} not found in chunk ${chunk.id}`);
373
+ }
374
+
375
+ /**
376
+ * Get attachment URL for a given document and attachment name
377
+ */
378
+ getAttachmentUrl(docId: string, attachmentName: string): string {
379
+ // Construct attachment path: attachments/{docId}/{attachmentName}.{ext}
380
+ // The exact filename will be resolved from the document's _attachments metadata
381
+ return `${this.basePath}/attachments/${docId}/${attachmentName}`;
382
+ }
383
+
384
+ /**
385
+ * Load attachment data from the document and get the correct file path
386
+ * Uses raw document access to avoid recursion with hydration
387
+ */
388
+ async getAttachmentPath(docId: string, attachmentName: string): Promise<string | null> {
389
+ try {
390
+ const doc = await this.getRawDocument(docId);
391
+ if (doc._attachments && doc._attachments[attachmentName]) {
392
+ const attachment = doc._attachments[attachmentName];
393
+ if (attachment.path) {
394
+ // Return the full path as stored in the document
395
+ return `${this.basePath}/${attachment.path}`;
396
+ }
397
+ }
398
+ return null;
399
+ } catch {
400
+ return null;
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Load attachment as a blob (for browser) or buffer (for Node.js)
406
+ */
407
+ async getAttachmentBlob(docId: string, attachmentName: string): Promise<Blob | Buffer | null> {
408
+ const attachmentPath = await this.getAttachmentPath(docId, attachmentName);
409
+ if (!attachmentPath) {
410
+ return null;
411
+ }
412
+
413
+ try {
414
+ // Check if we're in a Node.js environment with local files
415
+ if (this.isLocalPath(attachmentPath) && nodeFS) {
416
+ // Use fs for local file access (e.g., in tests)
417
+ const buffer = await nodeFS.promises.readFile(attachmentPath);
418
+ return buffer;
419
+ } else {
420
+ // Use fetch for URL-based access (e.g., in browser)
421
+ const response = await fetch(attachmentPath);
422
+ if (!response.ok) {
423
+ throw new Error(
424
+ `Failed to fetch attachment ${docId}/${attachmentName}: ${response.status} ${response.statusText}`
425
+ );
426
+ }
427
+ return await response.blob();
428
+ }
429
+ } catch (error) {
430
+ logger.error(`Failed to load attachment ${docId}/${attachmentName}:`, error);
431
+ return null;
432
+ }
433
+ }
434
+
435
+ /**
436
+ * Hydrate document attachments by converting file paths to blob URLs
437
+ */
438
+ private async hydrateAttachments<T = any>(doc: T): Promise<T> {
439
+ // logger.debug(`[hydrateAttachments] Starting hydration for doc: ${JSON.stringify(doc)}`);
440
+ const typedDoc = doc as any;
441
+
442
+ // If no attachments, return document as-is
443
+ if (!typedDoc._attachments) {
444
+ return doc;
445
+ }
446
+
447
+ // Clone the document to avoid mutating the cached version
448
+ const hydratedDoc = JSON.parse(JSON.stringify(doc));
449
+
450
+ // Process each attachment
451
+ for (const [attachmentName, attachment] of Object.entries(typedDoc._attachments)) {
452
+ // logger.debug(
453
+ // `[hydrateAttachments] Processing attachment: ${attachmentName} for doc ${typedDoc?._id}`
454
+ // );
455
+ const attachmentData = attachment as any;
456
+
457
+ // If attachment has a path, convert it to a blob URL
458
+ if (attachmentData.path) {
459
+ // logger.debug(
460
+ // `[hydrateAttachments] Attachment ${attachmentName} has path: ${attachmentData.path}. Attempting to get blob.`
461
+ // );
462
+ try {
463
+ const blob = await this.getAttachmentBlob(typedDoc._id, attachmentName);
464
+ if (blob) {
465
+ // logger.debug(
466
+ // `[hydrateAttachments] Successfully retrieved blob for ${typedDoc._id}/${attachmentName}. Size: ${blob instanceof Blob ? blob.size : (blob as Buffer).length}`
467
+ // );
468
+ // Create blob URL for browser rendering
469
+ if (typeof window !== 'undefined' && window.URL) {
470
+ // Store attachment data in PouchDB-compatible format
471
+ hydratedDoc._attachments[attachmentName] = {
472
+ ...attachmentData,
473
+ data: blob,
474
+ stub: false, // Indicates this contains actual data, not just metadata
475
+ };
476
+ // logger.debug(
477
+ // `[hydrateAttachments] Added blobUrl and blob to attachment ${attachmentName} for doc ${typedDoc._id}.`
478
+ // );
479
+ } else {
480
+ // In Node.js environment, just attach the buffer
481
+ hydratedDoc._attachments[attachmentName] = {
482
+ ...attachmentData,
483
+ buffer: blob, // Attach buffer for Node.js use
484
+ };
485
+ // logger.debug(
486
+ // `[hydrateAttachments] Added buffer to attachment ${attachmentName} for doc ${typedDoc._id}.`
487
+ // );
488
+ }
489
+ } else {
490
+ logger.warn(
491
+ `[hydrateAttachments] getAttachmentBlob returned null for ${typedDoc._id}/${attachmentName}. Skipping hydration for this attachment.`
492
+ );
493
+ }
494
+ } catch (error) {
495
+ logger.warn(
496
+ `[hydrateAttachments] Failed to hydrate attachment ${typedDoc._id}/${attachmentName}:`,
497
+ error
498
+ );
499
+ // Keep original attachment data if hydration fails
500
+ }
501
+ } else {
502
+ logger.debug(
503
+ `[hydrateAttachments] Attachment ${attachmentName} for doc ${typedDoc._id} has no path. Skipping blob conversion.`
504
+ );
505
+ }
506
+ }
507
+
508
+ // logger.debug(
509
+ // `[hydrateAttachments] Finished hydration for doc ${typedDoc?._id}. Returning hydrated doc: ${JSON.stringify(hydratedDoc)}`
510
+ // );
511
+ return hydratedDoc;
512
+ }
513
+
514
+ /**
515
+ * Clear all caches (useful for testing or memory management)
516
+ */
517
+ clearCaches(): void {
518
+ this.documentCache.clear();
519
+ this.chunkCache.clear();
520
+ this.indexCache.clear();
521
+ }
522
+
523
+ /**
524
+ * Get cache statistics for debugging
525
+ */
526
+ getCacheStats(): {
527
+ documents: number;
528
+ chunks: number;
529
+ indices: number;
530
+ } {
531
+ return {
532
+ documents: this.documentCache.size,
533
+ chunks: this.chunkCache.size,
534
+ indices: this.indexCache.size,
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Check if a path is a local file path (vs URL)
540
+ */
541
+ private isLocalPath(filePath: string): boolean {
542
+ // Check if it's an absolute path or doesn't start with http/https
543
+ return (
544
+ !filePath.startsWith('http://') &&
545
+ !filePath.startsWith('https://') &&
546
+ (pathUtils.isAbsolute(filePath) || filePath.startsWith('./') || filePath.startsWith('../'))
547
+ );
548
+ }
549
+ }