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.
- package/README.md +445 -213
- package/dist/components/Collection.svelte +150 -0
- package/dist/components/Collection.svelte.d.ts +27 -0
- package/dist/components/Ddoc.svelte +131 -0
- package/dist/components/Ddoc.svelte.d.ts +28 -0
- package/dist/components/Node.svelte +97 -0
- package/dist/components/Node.svelte.d.ts +23 -0
- package/dist/components/auth-guard.svelte +89 -0
- package/dist/components/auth-guard.svelte.d.ts +26 -0
- package/dist/components/custom-guard.svelte +122 -0
- package/dist/components/custom-guard.svelte.d.ts +31 -0
- package/dist/components/download-url.svelte +92 -0
- package/dist/components/download-url.svelte.d.ts +19 -0
- package/dist/components/firebase-app.svelte +30 -0
- package/dist/components/firebase-app.svelte.d.ts +7 -0
- package/dist/components/node-list.svelte +102 -0
- package/dist/components/node-list.svelte.d.ts +27 -0
- package/dist/components/signed-in.svelte +42 -0
- package/dist/components/signed-in.svelte.d.ts +11 -0
- package/dist/components/signed-out.svelte +42 -0
- package/dist/components/signed-out.svelte.d.ts +11 -0
- package/dist/components/storage-list.svelte +97 -0
- package/dist/components/storage-list.svelte.d.ts +26 -0
- package/dist/components/upload-task.svelte +108 -0
- package/dist/components/upload-task.svelte.d.ts +24 -0
- package/dist/config.js +17 -39
- package/dist/firebase.d.ts +43 -21
- package/dist/firebase.js +121 -35
- package/dist/index.d.ts +21 -13
- package/dist/index.js +27 -15
- package/dist/services/auth.d.ts +389 -0
- package/dist/services/auth.js +824 -0
- package/dist/services/collection.svelte.d.ts +286 -0
- package/dist/services/collection.svelte.js +871 -0
- package/dist/services/document.svelte.d.ts +288 -0
- package/dist/services/document.svelte.js +555 -0
- package/dist/services/mutations.d.ts +336 -0
- package/dist/services/mutations.js +1079 -0
- package/dist/services/presence.svelte.d.ts +141 -0
- package/dist/services/presence.svelte.js +727 -0
- package/dist/{realtime → services}/realtime.svelte.d.ts +3 -1
- package/dist/{realtime → services}/realtime.svelte.js +13 -7
- package/dist/services/storage.svelte.d.ts +257 -0
- package/dist/services/storage.svelte.js +374 -0
- package/dist/services/user.svelte.d.ts +290 -0
- package/dist/services/user.svelte.js +533 -0
- package/dist/types/auth.d.ts +158 -0
- package/dist/types/auth.js +106 -0
- package/dist/types/collection.d.ts +360 -0
- package/dist/types/collection.js +167 -0
- package/dist/types/document.d.ts +342 -0
- package/dist/types/document.js +148 -0
- package/dist/types/firebase.d.ts +44 -0
- package/dist/types/firebase.js +33 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +4 -0
- package/dist/types/mutations.d.ts +387 -0
- package/dist/types/mutations.js +205 -0
- package/dist/types/presence.d.ts +282 -0
- package/dist/types/presence.js +80 -0
- package/dist/utils/errors.d.ts +21 -0
- package/dist/utils/errors.js +35 -0
- package/dist/utils/firestore.d.ts +9 -0
- package/dist/utils/firestore.js +33 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/providers.d.ts +16 -0
- package/dist/utils/providers.js +30 -0
- package/dist/utils/user.d.ts +8 -0
- package/dist/utils/user.js +29 -0
- package/package.json +65 -65
- package/dist/auth/auth.d.ts +0 -117
- package/dist/auth/auth.js +0 -194
- package/dist/auth/presence.svelte.d.ts +0 -139
- package/dist/auth/presence.svelte.js +0 -373
- package/dist/auth/user.svelte.d.ts +0 -112
- package/dist/auth/user.svelte.js +0 -155
- package/dist/firestore/awaitable-doc.svelte.d.ts +0 -141
- package/dist/firestore/awaitable-doc.svelte.js +0 -183
- package/dist/firestore/batch-mutations.svelte.d.ts +0 -140
- package/dist/firestore/batch-mutations.svelte.js +0 -218
- package/dist/firestore/collection-group.svelte.d.ts +0 -78
- package/dist/firestore/collection-group.svelte.js +0 -120
- package/dist/firestore/collection.svelte.d.ts +0 -96
- package/dist/firestore/collection.svelte.js +0 -137
- package/dist/firestore/doc.svelte.d.ts +0 -90
- package/dist/firestore/doc.svelte.js +0 -131
- package/dist/firestore/document-mutations.svelte.d.ts +0 -164
- package/dist/firestore/document-mutations.svelte.js +0 -273
- package/dist/storage/download-url.svelte.d.ts +0 -83
- package/dist/storage/download-url.svelte.js +0 -114
- package/dist/storage/storage-list.svelte.d.ts +0 -89
- package/dist/storage/storage-list.svelte.js +0 -123
- package/dist/storage/upload-task.svelte.d.ts +0 -94
- 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
|
+
}
|