svelte-firekit 0.0.25 → 0.1.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/README.md +445 -213
  2. package/dist/components/Collection.svelte +150 -0
  3. package/dist/components/Collection.svelte.d.ts +27 -0
  4. package/dist/components/Ddoc.svelte +131 -0
  5. package/dist/components/Ddoc.svelte.d.ts +28 -0
  6. package/dist/components/Node.svelte +97 -0
  7. package/dist/components/Node.svelte.d.ts +23 -0
  8. package/dist/components/auth-guard.svelte +89 -0
  9. package/dist/components/auth-guard.svelte.d.ts +26 -0
  10. package/dist/components/custom-guard.svelte +122 -0
  11. package/dist/components/custom-guard.svelte.d.ts +31 -0
  12. package/dist/components/download-url.svelte +92 -0
  13. package/dist/components/download-url.svelte.d.ts +19 -0
  14. package/dist/components/firebase-app.svelte +30 -0
  15. package/dist/components/firebase-app.svelte.d.ts +7 -0
  16. package/dist/components/node-list.svelte +102 -0
  17. package/dist/components/node-list.svelte.d.ts +27 -0
  18. package/dist/components/signed-in.svelte +42 -0
  19. package/dist/components/signed-in.svelte.d.ts +11 -0
  20. package/dist/components/signed-out.svelte +42 -0
  21. package/dist/components/signed-out.svelte.d.ts +11 -0
  22. package/dist/components/storage-list.svelte +97 -0
  23. package/dist/components/storage-list.svelte.d.ts +26 -0
  24. package/dist/components/upload-task.svelte +108 -0
  25. package/dist/components/upload-task.svelte.d.ts +24 -0
  26. package/dist/config.js +17 -39
  27. package/dist/firebase.d.ts +43 -21
  28. package/dist/firebase.js +121 -35
  29. package/dist/index.d.ts +21 -13
  30. package/dist/index.js +27 -15
  31. package/dist/services/auth.d.ts +389 -0
  32. package/dist/services/auth.js +824 -0
  33. package/dist/services/collection.svelte.d.ts +286 -0
  34. package/dist/services/collection.svelte.js +871 -0
  35. package/dist/services/document.svelte.d.ts +288 -0
  36. package/dist/services/document.svelte.js +555 -0
  37. package/dist/services/mutations.d.ts +336 -0
  38. package/dist/services/mutations.js +1079 -0
  39. package/dist/services/presence.svelte.d.ts +141 -0
  40. package/dist/services/presence.svelte.js +727 -0
  41. package/dist/{realtime → services}/realtime.svelte.d.ts +3 -1
  42. package/dist/{realtime → services}/realtime.svelte.js +13 -7
  43. package/dist/services/storage.svelte.d.ts +257 -0
  44. package/dist/services/storage.svelte.js +374 -0
  45. package/dist/services/user.svelte.d.ts +290 -0
  46. package/dist/services/user.svelte.js +533 -0
  47. package/dist/types/auth.d.ts +158 -0
  48. package/dist/types/auth.js +106 -0
  49. package/dist/types/collection.d.ts +360 -0
  50. package/dist/types/collection.js +167 -0
  51. package/dist/types/document.d.ts +342 -0
  52. package/dist/types/document.js +148 -0
  53. package/dist/types/firebase.d.ts +44 -0
  54. package/dist/types/firebase.js +33 -0
  55. package/dist/types/index.d.ts +6 -0
  56. package/dist/types/index.js +4 -0
  57. package/dist/types/mutations.d.ts +387 -0
  58. package/dist/types/mutations.js +205 -0
  59. package/dist/types/presence.d.ts +282 -0
  60. package/dist/types/presence.js +80 -0
  61. package/dist/utils/errors.d.ts +21 -0
  62. package/dist/utils/errors.js +35 -0
  63. package/dist/utils/firestore.d.ts +9 -0
  64. package/dist/utils/firestore.js +33 -0
  65. package/dist/utils/index.d.ts +4 -0
  66. package/dist/utils/index.js +8 -0
  67. package/dist/utils/providers.d.ts +16 -0
  68. package/dist/utils/providers.js +30 -0
  69. package/dist/utils/user.d.ts +8 -0
  70. package/dist/utils/user.js +29 -0
  71. package/package.json +65 -65
  72. package/dist/auth/auth.d.ts +0 -117
  73. package/dist/auth/auth.js +0 -194
  74. package/dist/auth/presence.svelte.d.ts +0 -139
  75. package/dist/auth/presence.svelte.js +0 -373
  76. package/dist/auth/user.svelte.d.ts +0 -112
  77. package/dist/auth/user.svelte.js +0 -155
  78. package/dist/firestore/awaitable-doc.svelte.d.ts +0 -141
  79. package/dist/firestore/awaitable-doc.svelte.js +0 -183
  80. package/dist/firestore/batch-mutations.svelte.d.ts +0 -140
  81. package/dist/firestore/batch-mutations.svelte.js +0 -218
  82. package/dist/firestore/collection-group.svelte.d.ts +0 -78
  83. package/dist/firestore/collection-group.svelte.js +0 -120
  84. package/dist/firestore/collection.svelte.d.ts +0 -96
  85. package/dist/firestore/collection.svelte.js +0 -137
  86. package/dist/firestore/doc.svelte.d.ts +0 -90
  87. package/dist/firestore/doc.svelte.js +0 -131
  88. package/dist/firestore/document-mutations.svelte.d.ts +0 -164
  89. package/dist/firestore/document-mutations.svelte.js +0 -273
  90. package/dist/storage/download-url.svelte.d.ts +0 -83
  91. package/dist/storage/download-url.svelte.js +0 -114
  92. package/dist/storage/storage-list.svelte.d.ts +0 -89
  93. package/dist/storage/storage-list.svelte.js +0 -123
  94. package/dist/storage/upload-task.svelte.d.ts +0 -94
  95. package/dist/storage/upload-task.svelte.js +0 -138
@@ -0,0 +1,871 @@
1
+ /**
2
+ * @fileoverview FirekitCollection - Optimized collection management for Svelte applications
3
+ * @module FirekitCollection
4
+ * @version 1.0.0
5
+ */
6
+ import { collection, collectionGroup, query, onSnapshot, getDocs, where, orderBy, limit, startAt, startAfter, endAt, endBefore } from 'firebase/firestore';
7
+ import { firebaseService } from '../firebase.js';
8
+ import { browser } from '$app/environment';
9
+ import { CollectionErrorCode, CollectionError } from '../types/collection.js';
10
+ /**
11
+ * Query builder implementation for type-safe query construction
12
+ */
13
+ class FirekitQueryBuilder {
14
+ constraints = [];
15
+ where(field, operator, value) {
16
+ this.constraints.push(where(field, operator, value));
17
+ return this;
18
+ }
19
+ orderBy(field, direction = 'asc') {
20
+ this.constraints.push(orderBy(field, direction));
21
+ return this;
22
+ }
23
+ limit(count) {
24
+ this.constraints.push(limit(count));
25
+ return this;
26
+ }
27
+ startAt(...values) {
28
+ this.constraints.push(startAt(...values));
29
+ return this;
30
+ }
31
+ startAfter(...values) {
32
+ this.constraints.push(startAfter(...values));
33
+ return this;
34
+ }
35
+ endAt(...values) {
36
+ this.constraints.push(endAt(...values));
37
+ return this;
38
+ }
39
+ endBefore(...values) {
40
+ this.constraints.push(endBefore(...values));
41
+ return this;
42
+ }
43
+ build() {
44
+ return [...this.constraints];
45
+ }
46
+ }
47
+ /**
48
+ * Comprehensive Firestore collection management with real-time updates and advanced features.
49
+ * Uses Svelte 5 runes for optimal reactivity and performance.
50
+ *
51
+ * @class FirekitCollection
52
+ * @template T Document data type
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * interface User {
57
+ * id: string;
58
+ * name: string;
59
+ * email: string;
60
+ * active: boolean;
61
+ * }
62
+ *
63
+ * // Simple collection subscription
64
+ * const users = firekitCollection<User>('users');
65
+ *
66
+ * // With query constraints
67
+ * const activeUsers = firekitCollection<User>('users',
68
+ * where('active', '==', true),
69
+ * orderBy('name'),
70
+ * limit(10)
71
+ * );
72
+ *
73
+ * // With advanced options
74
+ * const paginatedUsers = firekitCollection<User>('users', {
75
+ * pagination: { enabled: true, pageSize: 20 },
76
+ * cache: { enabled: true, ttl: 300000 },
77
+ * transform: (doc) => ({ ...doc, displayName: doc.name.toUpperCase() })
78
+ * });
79
+ *
80
+ * // Access reactive state
81
+ * $: if (users.loading) {
82
+ * console.log('Loading...');
83
+ * } else if (users.error) {
84
+ * console.error('Error:', users.error);
85
+ * } else {
86
+ * console.log('Users:', users.data);
87
+ * }
88
+ * ```
89
+ */
90
+ class FirekitCollection {
91
+ // Reactive state using Svelte 5 runes
92
+ _data = $state([]);
93
+ _loading = $state(true);
94
+ _initialized = $state(false);
95
+ _error = $state(null);
96
+ _lastUpdated = $state(null);
97
+ // Internal state
98
+ collectionRef = null;
99
+ queryRef = null;
100
+ unsubscribe = null;
101
+ eventListeners = new Set();
102
+ options;
103
+ stats = this.initializeStats();
104
+ cache = new Map();
105
+ collectionPath;
106
+ /**
107
+ * Creates a collection subscription
108
+ *
109
+ * @param path Collection path
110
+ * @param constraintsOrOptions Query constraints or collection options
111
+ * @param additionalConstraints Additional constraints if options were provided
112
+ */
113
+ constructor(path, constraintsOrOptions, ...additionalConstraints) {
114
+ this.collectionPath = path;
115
+ // Parse constructor arguments
116
+ if (Array.isArray(constraintsOrOptions)) {
117
+ // Old style: path, ...constraints
118
+ this.options = {};
119
+ this.initializeCollection([...constraintsOrOptions, ...additionalConstraints]);
120
+ }
121
+ else {
122
+ // New style: path, options, ...constraints
123
+ this.options = constraintsOrOptions || {};
124
+ // Only use additionalConstraints since options object doesn't contain constraints
125
+ this.initializeCollection(additionalConstraints);
126
+ }
127
+ }
128
+ /**
129
+ * Initialize statistics object
130
+ */
131
+ initializeStats() {
132
+ return {
133
+ totalDocuments: 0,
134
+ readCount: 0,
135
+ writeCount: 0,
136
+ cacheHitRate: 0,
137
+ averageQueryTime: 0,
138
+ lastActivity: new Date(),
139
+ memoryUsage: 0
140
+ };
141
+ }
142
+ /**
143
+ * Initialize collection subscription
144
+ */
145
+ async initializeCollection(constraints) {
146
+ if (!browser)
147
+ return;
148
+ try {
149
+ const firestore = firebaseService.getDbInstance();
150
+ if (!firestore) {
151
+ throw new CollectionError(CollectionErrorCode.COLLECTION_UNAVAILABLE, 'Firestore instance not available', this.collectionPath);
152
+ }
153
+ // Check cache first
154
+ const cacheKey = this.getCacheKey(constraints);
155
+ if (this.options.cache?.enabled && this.isCacheValid(cacheKey)) {
156
+ const cached = this.cache.get(cacheKey);
157
+ this._data = cached.data;
158
+ this._loading = false;
159
+ this._initialized = true;
160
+ this._lastUpdated = cached.timestamp;
161
+ this.emitEvent({
162
+ type: 'cache_hit',
163
+ data: cached.data,
164
+ timestamp: new Date(),
165
+ path: this.collectionPath
166
+ });
167
+ this.stats.cacheHitRate = (this.stats.cacheHitRate + 1) / 2;
168
+ }
169
+ // Create collection reference
170
+ this.collectionRef = collection(firestore, this.collectionPath);
171
+ // Create query with constraints
172
+ this.queryRef =
173
+ constraints.length > 0 ? query(this.collectionRef, ...constraints) : this.collectionRef;
174
+ // Set up real-time listener or one-time fetch
175
+ if (this.options.realtime !== false) {
176
+ this.setupRealtimeListener();
177
+ }
178
+ else {
179
+ await this.fetchOnce();
180
+ }
181
+ }
182
+ catch (error) {
183
+ this.handleError(error);
184
+ }
185
+ }
186
+ /**
187
+ * Set up real-time document listener
188
+ */
189
+ setupRealtimeListener() {
190
+ if (!this.queryRef)
191
+ return;
192
+ this.emitEvent({
193
+ type: 'loading_started',
194
+ timestamp: new Date(),
195
+ path: this.collectionPath
196
+ });
197
+ const options = {
198
+ includeMetadataChanges: this.options.includeMetadata || false
199
+ };
200
+ this.unsubscribe = onSnapshot(this.queryRef, options, (snapshot) => {
201
+ this.processSnapshot(snapshot);
202
+ }, (error) => {
203
+ this.handleError(error);
204
+ });
205
+ }
206
+ /**
207
+ * Fetch collection data once (no real-time updates)
208
+ */
209
+ async fetchOnce() {
210
+ if (!this.queryRef)
211
+ return;
212
+ try {
213
+ this._loading = true;
214
+ this.emitEvent({
215
+ type: 'loading_started',
216
+ timestamp: new Date(),
217
+ path: this.collectionPath
218
+ });
219
+ const startTime = Date.now();
220
+ const snapshot = await getDocs(this.queryRef);
221
+ const queryTime = Date.now() - startTime;
222
+ this.stats.averageQueryTime = (this.stats.averageQueryTime + queryTime) / 2;
223
+ this.processSnapshot(snapshot);
224
+ }
225
+ catch (error) {
226
+ this.handleError(error);
227
+ }
228
+ }
229
+ /**
230
+ * Process query snapshot and update state
231
+ */
232
+ processSnapshot(snapshot) {
233
+ try {
234
+ const startTime = Date.now();
235
+ // Extract documents with ID
236
+ let documents = snapshot.docs.map((doc) => {
237
+ const data = doc.data();
238
+ return { id: doc.id, ...data };
239
+ });
240
+ // Apply transform function if provided
241
+ if (this.options.transform) {
242
+ documents = documents.map(this.options.transform);
243
+ }
244
+ // Apply filter function if provided
245
+ if (this.options.filter) {
246
+ documents = documents.filter(this.options.filter);
247
+ }
248
+ // Apply sort function if provided
249
+ if (this.options.sort) {
250
+ documents = documents.sort(this.options.sort);
251
+ }
252
+ // Track document changes for events
253
+ const changes = this.calculateChanges(this._data, documents);
254
+ // Update reactive state
255
+ this._data = documents;
256
+ this._loading = false;
257
+ this._initialized = true;
258
+ this._error = null;
259
+ this._lastUpdated = new Date();
260
+ // Update statistics
261
+ this.stats.totalDocuments = documents.length;
262
+ this.stats.readCount++;
263
+ this.stats.lastActivity = new Date();
264
+ this.stats.memoryUsage = this.calculateMemoryUsage(documents);
265
+ // Update cache if enabled
266
+ if (this.options.cache?.enabled) {
267
+ const cacheKey = this.getCacheKey([]);
268
+ this.cache.set(cacheKey, {
269
+ data: documents,
270
+ timestamp: new Date()
271
+ });
272
+ this.cleanupCache();
273
+ }
274
+ // Emit events
275
+ this.emitEvent({
276
+ type: 'data_changed',
277
+ data: documents,
278
+ changes,
279
+ timestamp: new Date(),
280
+ path: this.collectionPath
281
+ });
282
+ this.emitEvent({
283
+ type: 'loading_finished',
284
+ data: {
285
+ documentCount: documents.length,
286
+ processingTime: Date.now() - startTime
287
+ },
288
+ timestamp: new Date(),
289
+ path: this.collectionPath
290
+ });
291
+ // Emit individual change events
292
+ changes.forEach((change) => {
293
+ this.emitEvent({
294
+ type: `document_${change.type}`,
295
+ data: change.doc,
296
+ changes: [change],
297
+ timestamp: new Date(),
298
+ path: this.collectionPath
299
+ });
300
+ });
301
+ }
302
+ catch (error) {
303
+ this.handleError(error);
304
+ }
305
+ }
306
+ /**
307
+ * Calculate changes between old and new document arrays
308
+ */
309
+ calculateChanges(oldDocs, newDocs) {
310
+ const changes = [];
311
+ const oldMap = new Map(oldDocs.map((doc, index) => [doc.id, { doc, index }]));
312
+ const newMap = new Map(newDocs.map((doc, index) => [doc.id, { doc, index }]));
313
+ // Find added and modified documents
314
+ newDocs.forEach((newDoc, newIndex) => {
315
+ const id = newDoc.id;
316
+ const oldEntry = oldMap.get(id);
317
+ if (!oldEntry) {
318
+ // Document was added
319
+ changes.push({
320
+ type: 'added',
321
+ doc: newDoc,
322
+ oldIndex: -1,
323
+ newIndex,
324
+ timestamp: new Date()
325
+ });
326
+ }
327
+ else if (JSON.stringify(oldEntry.doc) !== JSON.stringify(newDoc)) {
328
+ // Document was modified
329
+ changes.push({
330
+ type: 'modified',
331
+ doc: newDoc,
332
+ oldIndex: oldEntry.index,
333
+ newIndex,
334
+ timestamp: new Date()
335
+ });
336
+ }
337
+ });
338
+ // Find removed documents
339
+ oldDocs.forEach((oldDoc, oldIndex) => {
340
+ const id = oldDoc.id;
341
+ if (!newMap.has(id)) {
342
+ changes.push({
343
+ type: 'removed',
344
+ doc: oldDoc,
345
+ oldIndex,
346
+ newIndex: -1,
347
+ timestamp: new Date()
348
+ });
349
+ }
350
+ });
351
+ return changes;
352
+ }
353
+ /**
354
+ * Handle and process errors
355
+ */
356
+ handleError(error) {
357
+ let collectionError;
358
+ if (error instanceof CollectionError) {
359
+ collectionError = error;
360
+ }
361
+ else {
362
+ // Map Firestore errors to CollectionError
363
+ const code = this.mapFirestoreErrorCode(error.code);
364
+ collectionError = new CollectionError(code, error.message || 'An unknown error occurred', this.collectionPath, [], error);
365
+ }
366
+ this._error = collectionError;
367
+ this._loading = false;
368
+ this.emitEvent({
369
+ type: 'error',
370
+ error: collectionError,
371
+ timestamp: new Date(),
372
+ path: this.collectionPath
373
+ });
374
+ console.error('FirekitCollection error:', collectionError);
375
+ }
376
+ /**
377
+ * Map Firestore error codes to CollectionErrorCode
378
+ */
379
+ mapFirestoreErrorCode(firestoreCode) {
380
+ switch (firestoreCode) {
381
+ case 'permission-denied':
382
+ return CollectionErrorCode.PERMISSION_DENIED;
383
+ case 'not-found':
384
+ return CollectionErrorCode.NOT_FOUND;
385
+ case 'unavailable':
386
+ return CollectionErrorCode.UNAVAILABLE;
387
+ case 'deadline-exceeded':
388
+ return CollectionErrorCode.DEADLINE_EXCEEDED;
389
+ case 'unauthenticated':
390
+ return CollectionErrorCode.UNAUTHENTICATED;
391
+ case 'resource-exhausted':
392
+ return CollectionErrorCode.RESOURCE_EXHAUSTED;
393
+ case 'failed-precondition':
394
+ return CollectionErrorCode.FAILED_PRECONDITION;
395
+ case 'aborted':
396
+ return CollectionErrorCode.ABORTED;
397
+ case 'out-of-range':
398
+ return CollectionErrorCode.OUT_OF_RANGE;
399
+ case 'unimplemented':
400
+ return CollectionErrorCode.UNIMPLEMENTED;
401
+ case 'internal':
402
+ return CollectionErrorCode.INTERNAL_ERROR;
403
+ case 'data-loss':
404
+ return CollectionErrorCode.DATA_LOSS;
405
+ case 'cancelled':
406
+ return CollectionErrorCode.CANCELLED;
407
+ default:
408
+ return CollectionErrorCode.UNKNOWN;
409
+ }
410
+ }
411
+ /**
412
+ * Generate cache key for query
413
+ */
414
+ getCacheKey(constraints) {
415
+ if (this.options.cache?.customKey) {
416
+ return this.options.cache.customKey(this.collectionPath, constraints);
417
+ }
418
+ // More reliable constraint serialization
419
+ const constraintString = constraints
420
+ .map((c) => {
421
+ try {
422
+ return JSON.stringify(c);
423
+ }
424
+ catch {
425
+ return c.toString();
426
+ }
427
+ })
428
+ .join('|');
429
+ return `${this.collectionPath}:${constraintString}`;
430
+ }
431
+ /**
432
+ * Check if cache entry is still valid
433
+ */
434
+ isCacheValid(cacheKey) {
435
+ const cached = this.cache.get(cacheKey);
436
+ if (!cached)
437
+ return false;
438
+ const ttl = this.options.cache?.ttl || 300000; // 5 minutes default
439
+ return Date.now() - cached.timestamp.getTime() < ttl;
440
+ }
441
+ /**
442
+ * Clean up expired cache entries
443
+ */
444
+ cleanupCache() {
445
+ const maxSize = this.options.cache?.maxSize || 100;
446
+ const ttl = this.options.cache?.ttl || 300000;
447
+ const now = Date.now();
448
+ // Remove expired entries
449
+ for (const [key, entry] of this.cache.entries()) {
450
+ if (now - entry.timestamp.getTime() > ttl) {
451
+ this.cache.delete(key);
452
+ }
453
+ }
454
+ // Remove oldest entries if cache is too large
455
+ if (this.cache.size > maxSize) {
456
+ const entries = Array.from(this.cache.entries()).sort((a, b) => a[1].timestamp.getTime() - b[1].timestamp.getTime());
457
+ const toRemove = entries.slice(0, this.cache.size - maxSize);
458
+ toRemove.forEach(([key]) => this.cache.delete(key));
459
+ }
460
+ }
461
+ /**
462
+ * Calculate memory usage of documents
463
+ */
464
+ calculateMemoryUsage(documents) {
465
+ try {
466
+ return JSON.stringify(documents).length * 2; // Rough estimate
467
+ }
468
+ catch {
469
+ return 0;
470
+ }
471
+ }
472
+ /**
473
+ * Emit event to all listeners
474
+ */
475
+ emitEvent(event) {
476
+ this.eventListeners.forEach((callback) => {
477
+ try {
478
+ callback(event);
479
+ }
480
+ catch (error) {
481
+ console.error('Error in collection event listener:', error);
482
+ }
483
+ });
484
+ }
485
+ // ========================================
486
+ // REACTIVE GETTERS (Public API)
487
+ // ========================================
488
+ /**
489
+ * Get current collection data
490
+ */
491
+ get data() {
492
+ return this._data;
493
+ }
494
+ /**
495
+ * Get loading state
496
+ */
497
+ get loading() {
498
+ return this._loading;
499
+ }
500
+ /**
501
+ * Get initialization state
502
+ */
503
+ get initialized() {
504
+ return this._initialized;
505
+ }
506
+ /**
507
+ * Get error state
508
+ */
509
+ get error() {
510
+ return this._error;
511
+ }
512
+ /**
513
+ * Check if collection is empty
514
+ */
515
+ get empty() {
516
+ return this._data.length === 0;
517
+ }
518
+ /**
519
+ * Get number of documents
520
+ */
521
+ get size() {
522
+ return this._data.length;
523
+ }
524
+ /**
525
+ * Get last update timestamp
526
+ */
527
+ get lastUpdated() {
528
+ return this._lastUpdated;
529
+ }
530
+ /**
531
+ * Get collection reference
532
+ */
533
+ get ref() {
534
+ if (!this.collectionRef) {
535
+ throw new CollectionError(CollectionErrorCode.REFERENCE_UNAVAILABLE, 'Collection reference not available', this.collectionPath);
536
+ }
537
+ return this.collectionRef;
538
+ }
539
+ /**
540
+ * Get query reference
541
+ */
542
+ get queryReference() {
543
+ if (!this.queryRef) {
544
+ throw new CollectionError(CollectionErrorCode.REFERENCE_UNAVAILABLE, 'Query reference not available', this.collectionPath);
545
+ }
546
+ return this.queryRef;
547
+ }
548
+ /**
549
+ * Get collection path
550
+ */
551
+ get path() {
552
+ return this.collectionPath;
553
+ }
554
+ /**
555
+ * Get collection state summary
556
+ */
557
+ get state() {
558
+ return {
559
+ data: this._data,
560
+ loading: this._loading,
561
+ initialized: this._initialized,
562
+ error: this._error,
563
+ empty: this.empty,
564
+ size: this.size,
565
+ lastUpdated: this._lastUpdated
566
+ };
567
+ }
568
+ // ========================================
569
+ // PUBLIC METHODS
570
+ // ========================================
571
+ /**
572
+ * Manually refresh collection data
573
+ */
574
+ async refresh() {
575
+ if (!this.queryRef) {
576
+ throw new CollectionError(CollectionErrorCode.REFERENCE_UNAVAILABLE, 'Cannot refresh: query reference not available', this.collectionPath);
577
+ }
578
+ try {
579
+ this._loading = true;
580
+ const snapshot = await getDocs(this.queryRef);
581
+ this.processSnapshot(snapshot);
582
+ }
583
+ catch (error) {
584
+ this.handleError(error);
585
+ throw error;
586
+ }
587
+ }
588
+ /**
589
+ * Get fresh data from server (bypassing cache)
590
+ */
591
+ async getFromServer() {
592
+ if (!this.queryRef) {
593
+ throw new CollectionError(CollectionErrorCode.REFERENCE_UNAVAILABLE, 'Cannot fetch: query reference not available', this.collectionPath);
594
+ }
595
+ try {
596
+ const snapshot = await getDocs(this.queryRef);
597
+ return snapshot.docs.map((doc) => {
598
+ const data = doc.data();
599
+ return { id: doc.id, ...data };
600
+ });
601
+ }
602
+ catch (error) {
603
+ this.handleError(error);
604
+ throw error;
605
+ }
606
+ }
607
+ /**
608
+ * Add query constraints to existing query
609
+ */
610
+ addConstraints(...constraints) {
611
+ if (!this.collectionRef) {
612
+ throw new CollectionError(CollectionErrorCode.REFERENCE_UNAVAILABLE, 'Cannot add constraints: collection reference not available', this.collectionPath);
613
+ }
614
+ const newQuery = query(this.queryRef || this.collectionRef, ...constraints);
615
+ const newCollection = new FirekitCollection(this.collectionPath, this.options);
616
+ newCollection.queryRef = newQuery;
617
+ newCollection.initializeCollection([]);
618
+ return newCollection;
619
+ }
620
+ /**
621
+ * Create a new query builder for this collection
622
+ */
623
+ createQuery() {
624
+ return new FirekitQueryBuilder();
625
+ }
626
+ /**
627
+ * Apply new query constraints
628
+ */
629
+ withQuery(builder) {
630
+ return this.addConstraints(...builder.build());
631
+ }
632
+ /**
633
+ * Filter documents by predicate
634
+ */
635
+ filter(predicate) {
636
+ return this._data.filter(predicate);
637
+ }
638
+ /**
639
+ * Find first document matching predicate
640
+ */
641
+ find(predicate) {
642
+ return this._data.find(predicate);
643
+ }
644
+ /**
645
+ * Find document by ID
646
+ */
647
+ findById(id) {
648
+ return this._data.find((doc) => doc.id === id);
649
+ }
650
+ /**
651
+ * Sort documents by field or custom function
652
+ */
653
+ sort(compareFn) {
654
+ return [...this._data].sort(compareFn);
655
+ }
656
+ /**
657
+ * Get paginated subset of documents
658
+ */
659
+ paginate(page, pageSize) {
660
+ const startIndex = (page - 1) * pageSize;
661
+ const endIndex = startIndex + pageSize;
662
+ return this._data.slice(startIndex, endIndex);
663
+ }
664
+ /**
665
+ * Group documents by field value
666
+ */
667
+ groupBy(field) {
668
+ const groups = new Map();
669
+ this._data.forEach((doc) => {
670
+ const key = doc[field];
671
+ if (!groups.has(key)) {
672
+ groups.set(key, []);
673
+ }
674
+ groups.get(key).push(doc);
675
+ });
676
+ return groups;
677
+ }
678
+ /**
679
+ * Get unique values for a field
680
+ */
681
+ unique(field) {
682
+ const values = this._data.map((doc) => doc[field]);
683
+ return Array.from(new Set(values));
684
+ }
685
+ /**
686
+ * Count documents matching predicate
687
+ */
688
+ count(predicate) {
689
+ return predicate ? this._data.filter(predicate).length : this._data.length;
690
+ }
691
+ /**
692
+ * Check if any document matches predicate
693
+ */
694
+ some(predicate) {
695
+ return this._data.some(predicate);
696
+ }
697
+ /**
698
+ * Check if all documents match predicate
699
+ */
700
+ every(predicate) {
701
+ return this._data.every(predicate);
702
+ }
703
+ /**
704
+ * Switch between realtime and one-time fetch modes
705
+ */
706
+ setRealtimeMode(realtime) {
707
+ if (this.options.realtime === realtime)
708
+ return;
709
+ this.options.realtime = realtime;
710
+ // Clean up existing listener
711
+ if (this.unsubscribe) {
712
+ this.unsubscribe();
713
+ this.unsubscribe = null;
714
+ }
715
+ // Set up new mode
716
+ if (realtime) {
717
+ this.setupRealtimeListener();
718
+ }
719
+ }
720
+ /**
721
+ * Clear cache for this collection
722
+ */
723
+ clearCache() {
724
+ const cacheKey = this.getCacheKey([]);
725
+ this.cache.delete(cacheKey);
726
+ }
727
+ /**
728
+ * Get collection statistics
729
+ */
730
+ getStats() {
731
+ return { ...this.stats };
732
+ }
733
+ /**
734
+ * Reset statistics
735
+ */
736
+ resetStats() {
737
+ this.stats = this.initializeStats();
738
+ }
739
+ // ========================================
740
+ // EVENT MANAGEMENT
741
+ // ========================================
742
+ /**
743
+ * Add event listener
744
+ */
745
+ addEventListener(callback) {
746
+ this.eventListeners.add(callback);
747
+ return () => this.eventListeners.delete(callback);
748
+ }
749
+ /**
750
+ * Remove all event listeners
751
+ */
752
+ clearEventListeners() {
753
+ this.eventListeners.clear();
754
+ }
755
+ /**
756
+ * Wait for collection to initialize
757
+ */
758
+ async waitForInitialization() {
759
+ return new Promise((resolve) => {
760
+ if (this._initialized) {
761
+ resolve(this._data);
762
+ return;
763
+ }
764
+ const unsubscribe = this.addEventListener((event) => {
765
+ if (event.type === 'data_changed' || event.type === 'error') {
766
+ unsubscribe();
767
+ resolve(this._data);
768
+ }
769
+ });
770
+ });
771
+ }
772
+ // ========================================
773
+ // CLEANUP
774
+ // ========================================
775
+ /**
776
+ * Dispose of all resources and cleanup
777
+ */
778
+ dispose() {
779
+ // Unsubscribe from real-time updates
780
+ if (this.unsubscribe) {
781
+ this.unsubscribe();
782
+ this.unsubscribe = null;
783
+ }
784
+ // Clear cache
785
+ this.cache.clear();
786
+ // Clear event listeners
787
+ this.eventListeners.clear();
788
+ // Reset state
789
+ this._data = [];
790
+ this._loading = false;
791
+ this._initialized = false;
792
+ this._error = null;
793
+ this._lastUpdated = null;
794
+ }
795
+ }
796
+ /**
797
+ * Collection Group implementation for querying across multiple collections
798
+ */
799
+ class FirekitCollectionGroup extends FirekitCollection {
800
+ constructor(collectionId, constraintsOrOptions, ...additionalConstraints) {
801
+ // Initialize with empty options to avoid parent constructor issues
802
+ super(`__collection_group__${collectionId}`, {});
803
+ // Parse constructor arguments properly
804
+ if (Array.isArray(constraintsOrOptions)) {
805
+ this.options = {};
806
+ this.initializeCollectionGroup(collectionId, [
807
+ ...constraintsOrOptions,
808
+ ...additionalConstraints
809
+ ]);
810
+ }
811
+ else {
812
+ this.options = constraintsOrOptions || {};
813
+ this.initializeCollectionGroup(collectionId, additionalConstraints);
814
+ }
815
+ }
816
+ /**
817
+ * Initialize collection group subscription
818
+ */
819
+ async initializeCollectionGroup(collectionId, constraints) {
820
+ if (!browser)
821
+ return;
822
+ try {
823
+ const firestore = firebaseService.getDbInstance();
824
+ if (!firestore) {
825
+ throw new CollectionError(CollectionErrorCode.COLLECTION_UNAVAILABLE, 'Firestore instance not available', collectionId);
826
+ }
827
+ // Create collection group reference
828
+ const groupRef = collectionGroup(firestore, collectionId);
829
+ // Create query with constraints
830
+ this.queryRef =
831
+ constraints.length > 0
832
+ ? query(groupRef, ...constraints)
833
+ : groupRef;
834
+ // Set up real-time listener or one-time fetch
835
+ if (this.options.realtime !== false) {
836
+ this.setupRealtimeListener();
837
+ }
838
+ else {
839
+ await this.fetchOnce();
840
+ }
841
+ }
842
+ catch (error) {
843
+ this.handleError(error);
844
+ }
845
+ }
846
+ }
847
+ export function firekitCollection(path, constraintsOrOptions, ...additionalConstraints) {
848
+ if (Array.isArray(constraintsOrOptions)) {
849
+ return new FirekitCollection(path, [...constraintsOrOptions, ...additionalConstraints]);
850
+ }
851
+ else {
852
+ return new FirekitCollection(path, constraintsOrOptions, ...additionalConstraints);
853
+ }
854
+ }
855
+ /**
856
+ * Creates a one-time collection fetch (no real-time updates)
857
+ */
858
+ export function firekitCollectionOnce(path, ...constraints) {
859
+ return new FirekitCollection(path, { realtime: false }, ...constraints);
860
+ }
861
+ export function firekitCollectionGroup(collectionId, constraintsOrOptions, ...additionalConstraints) {
862
+ if (Array.isArray(constraintsOrOptions)) {
863
+ return new FirekitCollectionGroup(collectionId, [
864
+ ...constraintsOrOptions,
865
+ ...additionalConstraints
866
+ ]);
867
+ }
868
+ else {
869
+ return new FirekitCollectionGroup(collectionId, constraintsOrOptions, ...additionalConstraints);
870
+ }
871
+ }