svelte-firekit 0.0.25 → 0.1.1

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 +397 -0
  32. package/dist/services/auth.js +882 -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 +296 -0
  46. package/dist/services/user.svelte.js +609 -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 +64 -64
  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,727 @@
1
+ /**
2
+ * @fileoverview FirekitPresence - Clean presence tracking for Svelte applications
3
+ * @module FirekitPresence
4
+ * @version 1.0.0
5
+ */
6
+ import { ref, onValue, onDisconnect, set, get, serverTimestamp } from 'firebase/database';
7
+ import { firebaseService } from '../firebase.js';
8
+ import { browser } from '$app/environment';
9
+ import { PresenceErrorCode, PresenceError } from '../types/presence.js';
10
+ /**
11
+ * Handles geolocation tracking
12
+ */
13
+ class GeolocationService {
14
+ config;
15
+ watchId = null;
16
+ _hasConsent = $state(false);
17
+ _location = $state(null);
18
+ _error = $state(null);
19
+ constructor(config) {
20
+ this.config = config;
21
+ }
22
+ get hasConsent() {
23
+ return this._hasConsent;
24
+ }
25
+ get location() {
26
+ return this._location;
27
+ }
28
+ get error() {
29
+ return this._error;
30
+ }
31
+ /**
32
+ * Request user consent for location tracking
33
+ */
34
+ async requestConsent() {
35
+ if (!this.config.enabled || !browser)
36
+ return false;
37
+ try {
38
+ if (this.config.type === 'browser') {
39
+ const success = await new Promise((resolve) => {
40
+ navigator.geolocation.getCurrentPosition(() => resolve(true), () => resolve(false), {
41
+ timeout: this.config.timeout || 10000,
42
+ enableHighAccuracy: this.config.enableHighAccuracy || false
43
+ });
44
+ });
45
+ this._hasConsent = success;
46
+ return success;
47
+ }
48
+ this._hasConsent = true;
49
+ return true;
50
+ }
51
+ catch (error) {
52
+ this._error = error;
53
+ return false;
54
+ }
55
+ }
56
+ /**
57
+ * Get current location
58
+ */
59
+ async getCurrentLocation() {
60
+ if (!this.config.enabled || (this.config.requireConsent && !this._hasConsent)) {
61
+ return null;
62
+ }
63
+ try {
64
+ switch (this.config.type) {
65
+ case 'browser':
66
+ return this.getBrowserLocation();
67
+ case 'ip':
68
+ return this.getIPLocation();
69
+ case 'custom':
70
+ return this.getCustomLocation();
71
+ default:
72
+ return null;
73
+ }
74
+ }
75
+ catch (error) {
76
+ this._error = error;
77
+ return null;
78
+ }
79
+ }
80
+ /**
81
+ * Start watching location changes
82
+ */
83
+ startWatching(updateInterval) {
84
+ if (!this.config.enabled || this.watchId)
85
+ return;
86
+ const watchLocation = async () => {
87
+ const location = await this.getCurrentLocation();
88
+ if (location) {
89
+ this._location = location;
90
+ }
91
+ };
92
+ watchLocation();
93
+ this.watchId = window.setInterval(watchLocation, updateInterval);
94
+ }
95
+ /**
96
+ * Stop watching location changes
97
+ */
98
+ stopWatching() {
99
+ if (this.watchId) {
100
+ clearInterval(this.watchId);
101
+ this.watchId = null;
102
+ }
103
+ }
104
+ async getBrowserLocation() {
105
+ return new Promise((resolve) => {
106
+ navigator.geolocation.getCurrentPosition((position) => {
107
+ resolve({
108
+ latitude: position.coords.latitude,
109
+ longitude: position.coords.longitude,
110
+ accuracy: position.coords.accuracy,
111
+ altitude: position.coords.altitude || undefined,
112
+ altitudeAccuracy: position.coords.altitudeAccuracy || undefined,
113
+ heading: position.coords.heading || undefined,
114
+ speed: position.coords.speed || undefined,
115
+ lastUpdated: new Date().toISOString(),
116
+ source: 'browser'
117
+ });
118
+ }, () => resolve(null), {
119
+ timeout: this.config.timeout || 10000,
120
+ enableHighAccuracy: this.config.enableHighAccuracy || false,
121
+ maximumAge: this.config.maximumAge || 300000
122
+ });
123
+ });
124
+ }
125
+ async getIPLocation() {
126
+ if (!this.config.ipServiceUrl)
127
+ return null;
128
+ try {
129
+ const response = await fetch(this.config.ipServiceUrl);
130
+ const data = await response.json();
131
+ return {
132
+ latitude: data.latitude,
133
+ longitude: data.longitude,
134
+ accuracy: data.accuracy || undefined,
135
+ lastUpdated: new Date().toISOString(),
136
+ source: 'ip'
137
+ };
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ async getCustomLocation() {
144
+ if (!this.config.customGeolocationFn)
145
+ return null;
146
+ try {
147
+ const result = await this.config.customGeolocationFn();
148
+ return {
149
+ ...result,
150
+ lastUpdated: new Date().toISOString(),
151
+ source: 'custom'
152
+ };
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ dispose() {
159
+ this.stopWatching();
160
+ this._hasConsent = false;
161
+ this._location = null;
162
+ this._error = null;
163
+ }
164
+ }
165
+ /**
166
+ * Handles device information detection
167
+ */
168
+ class DeviceInfoService {
169
+ static getDeviceInfo() {
170
+ const userAgent = navigator.userAgent;
171
+ const platform = navigator.platform;
172
+ // Detect device type
173
+ const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
174
+ const isTablet = /iPad|Android(?=.*Tablet)|Windows NT.*Touch/i.test(userAgent);
175
+ let deviceType = 'unknown';
176
+ if (isTablet)
177
+ deviceType = 'tablet';
178
+ else if (isMobile)
179
+ deviceType = 'mobile';
180
+ else
181
+ deviceType = 'desktop';
182
+ // Detect browser and version
183
+ const browserMatch = userAgent.match(/(firefox|chrome|safari|opera|edge|msie|trident(?=\/))\/?\s*(\d+)/i);
184
+ const browser = browserMatch ? browserMatch[1] : 'Unknown';
185
+ const browserVersion = browserMatch ? browserMatch[2] : '';
186
+ // Detect OS and version
187
+ let os = 'Unknown';
188
+ let osVersion = '';
189
+ if (userAgent.includes('Windows NT')) {
190
+ os = 'Windows';
191
+ const winMatch = userAgent.match(/Windows NT ([\d.]+)/);
192
+ osVersion = winMatch ? winMatch[1] : '';
193
+ }
194
+ else if (userAgent.includes('Mac OS X')) {
195
+ os = 'macOS';
196
+ const macMatch = userAgent.match(/Mac OS X ([\d_]+)/);
197
+ osVersion = macMatch ? macMatch[1].replace(/_/g, '.') : '';
198
+ }
199
+ else if (userAgent.includes('Linux')) {
200
+ os = 'Linux';
201
+ }
202
+ else if (userAgent.includes('Android')) {
203
+ os = 'Android';
204
+ const androidMatch = userAgent.match(/Android ([\d.]+)/);
205
+ osVersion = androidMatch ? androidMatch[1] : '';
206
+ }
207
+ else if (userAgent.includes('iOS')) {
208
+ os = 'iOS';
209
+ const iosMatch = userAgent.match(/OS ([\d_]+)/);
210
+ osVersion = iosMatch ? iosMatch[1].replace(/_/g, '.') : '';
211
+ }
212
+ // Generate device ID
213
+ const deviceId = `${browser}-${os}-${platform}`.replace(/[^a-zA-Z0-9-]/g, '');
214
+ // Get screen resolution and timezone
215
+ const screenResolution = `${screen.width}x${screen.height}`;
216
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
217
+ return {
218
+ id: deviceId,
219
+ type: deviceType,
220
+ browser,
221
+ browserVersion,
222
+ os,
223
+ osVersion,
224
+ userAgent,
225
+ screenResolution,
226
+ timezone
227
+ };
228
+ }
229
+ }
230
+ /**
231
+ * Main presence tracking service
232
+ *
233
+ * @class FirekitPresence
234
+ * @example
235
+ * ```typescript
236
+ * import { firekitPresence } from 'svelte-firekit';
237
+ *
238
+ * // Initialize
239
+ * await firekitPresence.initialize(user, {
240
+ * geolocation: { enabled: true, type: 'browser' },
241
+ * sessionTTL: 30 * 60 * 1000
242
+ * });
243
+ *
244
+ * // Listen to events
245
+ * const unsubscribe = firekitPresence.addEventListener((event) => {
246
+ * console.log('Presence event:', event);
247
+ * });
248
+ *
249
+ * // Access reactive state
250
+ * $: console.log('Status:', firekitPresence.status);
251
+ * ```
252
+ */
253
+ class FirekitPresence {
254
+ static instance;
255
+ config = {
256
+ sessionTTL: 30 * 60 * 1000,
257
+ updateInterval: 60 * 1000,
258
+ trackDeviceInfo: true
259
+ };
260
+ geolocationService = null;
261
+ connectedListener = null;
262
+ eventListeners = new Set();
263
+ currentUser = null;
264
+ // Reactive state using Svelte 5 runes
265
+ _initialized = $state(false);
266
+ _status = $state('offline');
267
+ _loading = $state(false);
268
+ _error = $state(null);
269
+ _currentSession = $state(null);
270
+ _sessions = $state([]);
271
+ constructor() {
272
+ if (browser) {
273
+ this.setupVisibilityListener();
274
+ }
275
+ }
276
+ /**
277
+ * Gets singleton instance of FirekitPresence
278
+ */
279
+ static getInstance() {
280
+ if (!FirekitPresence.instance) {
281
+ FirekitPresence.instance = new FirekitPresence();
282
+ }
283
+ return FirekitPresence.instance;
284
+ }
285
+ // ========================================
286
+ // REACTIVE GETTERS
287
+ // ========================================
288
+ get initialized() {
289
+ return this._initialized;
290
+ }
291
+ get status() {
292
+ return this._status;
293
+ }
294
+ get loading() {
295
+ return this._loading;
296
+ }
297
+ get error() {
298
+ return this._error;
299
+ }
300
+ get currentSession() {
301
+ return this._currentSession;
302
+ }
303
+ get sessions() {
304
+ return this._sessions;
305
+ }
306
+ get location() {
307
+ return this.geolocationService?.location || null;
308
+ }
309
+ get hasLocationConsent() {
310
+ return this.geolocationService?.hasConsent || false;
311
+ }
312
+ // ========================================
313
+ // INITIALIZATION
314
+ // ========================================
315
+ /**
316
+ * Initialize presence tracking
317
+ * @param user Current authenticated user
318
+ * @param config Presence configuration options
319
+ */
320
+ async initialize(user, config) {
321
+ try {
322
+ if (!browser) {
323
+ throw new PresenceError(PresenceErrorCode.INITIALIZATION_FAILED, 'Presence service can only be initialized in browser environment');
324
+ }
325
+ if (this._initialized) {
326
+ console.warn('Presence service is already initialized');
327
+ return;
328
+ }
329
+ this._loading = true;
330
+ this.currentUser = user;
331
+ this.config = { ...this.config, ...config };
332
+ // Initialize geolocation service if enabled
333
+ if (this.config.geolocation?.enabled) {
334
+ this.geolocationService = new GeolocationService(this.config.geolocation);
335
+ if (this.config.geolocation.requireConsent) {
336
+ this.emitEvent({ type: 'consent_requested', timestamp: Date.now() });
337
+ const hasConsent = await this.geolocationService.requestConsent();
338
+ this.emitEvent({
339
+ type: hasConsent ? 'consent_granted' : 'consent_denied',
340
+ timestamp: Date.now()
341
+ });
342
+ }
343
+ // Start location tracking if consent granted
344
+ if (!this.config.geolocation.requireConsent || this.geolocationService.hasConsent) {
345
+ this.geolocationService.startWatching(this.config.updateInterval);
346
+ }
347
+ }
348
+ // Set up Firebase connection monitoring
349
+ await this.setupConnectionMonitoring();
350
+ this._initialized = true;
351
+ this.emitEvent({
352
+ type: 'init',
353
+ data: { userId: user.uid, config: this.config },
354
+ timestamp: Date.now()
355
+ });
356
+ }
357
+ catch (error) {
358
+ this._error =
359
+ error instanceof PresenceError
360
+ ? error
361
+ : new PresenceError(PresenceErrorCode.INITIALIZATION_FAILED, `Failed to initialize presence service: ${error.message}`, error);
362
+ this.emitEvent({
363
+ type: 'error',
364
+ error: this._error,
365
+ timestamp: Date.now()
366
+ });
367
+ throw this._error;
368
+ }
369
+ finally {
370
+ this._loading = false;
371
+ }
372
+ }
373
+ /**
374
+ * Set up Firebase connection monitoring
375
+ */
376
+ async setupConnectionMonitoring() {
377
+ const db = firebaseService.getDatabaseInstance();
378
+ if (!db) {
379
+ throw new PresenceError(PresenceErrorCode.DATABASE_ERROR, 'Firebase Database instance not available');
380
+ }
381
+ const connectedRef = ref(db, '.info/connected');
382
+ this.connectedListener = onValue(connectedRef, async (snapshot) => {
383
+ if (snapshot.val() === true) {
384
+ await this.setPresence('online');
385
+ await this.setupDisconnectHandler();
386
+ }
387
+ else {
388
+ await this.setPresence('offline');
389
+ }
390
+ });
391
+ }
392
+ /**
393
+ * Set up disconnect handler for graceful offline transitions
394
+ */
395
+ async setupDisconnectHandler() {
396
+ if (!this.currentUser || !this._currentSession)
397
+ return;
398
+ const db = firebaseService.getDatabaseInstance();
399
+ if (!db)
400
+ return;
401
+ const sessionPath = this.config.sessionPath || 'presence';
402
+ const sessionRef = ref(db, `${sessionPath}/${this.currentUser.uid}/sessions/${this._currentSession.id}`);
403
+ await onDisconnect(sessionRef).update({
404
+ status: 'offline',
405
+ lastSeen: serverTimestamp()
406
+ });
407
+ }
408
+ /**
409
+ * Set up page visibility listener
410
+ */
411
+ setupVisibilityListener() {
412
+ if (!browser)
413
+ return;
414
+ document.addEventListener('visibilitychange', async () => {
415
+ if (!this._initialized)
416
+ return;
417
+ if (document.visibilityState === 'hidden') {
418
+ await this.setPresence('away');
419
+ }
420
+ else {
421
+ await this.setPresence('online');
422
+ }
423
+ });
424
+ }
425
+ // ========================================
426
+ // PRESENCE MANAGEMENT
427
+ // ========================================
428
+ /**
429
+ * Set presence status
430
+ */
431
+ async setPresence(status) {
432
+ try {
433
+ if (!this.currentUser) {
434
+ throw new PresenceError(PresenceErrorCode.USER_NOT_AUTHENTICATED, 'No authenticated user found');
435
+ }
436
+ const db = firebaseService.getDatabaseInstance();
437
+ if (!db) {
438
+ throw new PresenceError(PresenceErrorCode.DATABASE_ERROR, 'Firebase Database instance not available');
439
+ }
440
+ // Get current location if available
441
+ const location = await this.geolocationService?.getCurrentLocation();
442
+ // Get device info if enabled
443
+ const device = this.config.trackDeviceInfo ? DeviceInfoService.getDeviceInfo() : undefined;
444
+ let session;
445
+ if (!this._currentSession) {
446
+ // Create new session
447
+ session = {
448
+ id: `${this.currentUser.uid}_${device?.id || Date.now()}`,
449
+ userId: this.currentUser.uid,
450
+ status,
451
+ createdAt: new Date().toISOString(),
452
+ lastSeen: new Date().toISOString(),
453
+ lastActivity: new Date().toISOString(),
454
+ ...(location && { location }),
455
+ ...(device && { device }),
456
+ ...(this.config.customMetadata && { metadata: this.config.customMetadata })
457
+ };
458
+ this.emitEvent({
459
+ type: 'session_created',
460
+ data: { session },
461
+ timestamp: Date.now(),
462
+ sessionId: session.id,
463
+ userId: session.userId
464
+ });
465
+ }
466
+ else {
467
+ // Update existing session
468
+ session = {
469
+ ...this._currentSession,
470
+ status,
471
+ lastSeen: new Date().toISOString(),
472
+ lastActivity: new Date().toISOString(),
473
+ ...(location && { location })
474
+ };
475
+ this.emitEvent({
476
+ type: 'session_updated',
477
+ data: { session, previousStatus: this._currentSession.status },
478
+ timestamp: Date.now(),
479
+ sessionId: session.id,
480
+ userId: session.userId
481
+ });
482
+ }
483
+ // Save session to Firebase
484
+ const sessionPath = this.config.sessionPath || 'presence';
485
+ const sessionRef = ref(db, `${sessionPath}/${this.currentUser.uid}/sessions/${session.id}`);
486
+ await set(sessionRef, session);
487
+ // Update local state
488
+ this._currentSession = session;
489
+ this._status = status;
490
+ // Load and update all sessions
491
+ await this.loadSessions();
492
+ this.emitEvent({
493
+ type: 'status_change',
494
+ data: { status, session, location },
495
+ timestamp: Date.now(),
496
+ sessionId: session.id,
497
+ userId: session.userId
498
+ });
499
+ // Emit location update if location changed
500
+ if (location) {
501
+ this.emitEvent({
502
+ type: 'location_update',
503
+ data: { location, session },
504
+ timestamp: Date.now(),
505
+ sessionId: session.id,
506
+ userId: session.userId
507
+ });
508
+ }
509
+ }
510
+ catch (error) {
511
+ this._error =
512
+ error instanceof PresenceError
513
+ ? error
514
+ : new PresenceError(PresenceErrorCode.DATABASE_ERROR, `Failed to set presence: ${error.message}`, error);
515
+ this.emitEvent({
516
+ type: 'error',
517
+ error: this._error,
518
+ timestamp: Date.now()
519
+ });
520
+ throw this._error;
521
+ }
522
+ }
523
+ /**
524
+ * Load all sessions for current user
525
+ */
526
+ async loadSessions() {
527
+ if (!this.currentUser)
528
+ return;
529
+ try {
530
+ const db = firebaseService.getDatabaseInstance();
531
+ if (!db)
532
+ return;
533
+ const sessionPath = this.config.sessionPath || 'presence';
534
+ const sessionsRef = ref(db, `${sessionPath}/${this.currentUser.uid}/sessions`);
535
+ const snapshot = await get(sessionsRef);
536
+ if (snapshot.exists()) {
537
+ const sessionsData = snapshot.val();
538
+ let sessions = Object.values(sessionsData);
539
+ // Clean up stale sessions
540
+ if (this.config.sessionTTL) {
541
+ const cutoffTime = new Date(Date.now() - this.config.sessionTTL).toISOString();
542
+ const expiredSessions = sessions.filter((session) => session.lastSeen < cutoffTime);
543
+ sessions = sessions.filter((session) => session.lastSeen >= cutoffTime);
544
+ // Remove stale sessions from database and emit events
545
+ for (const expiredSession of expiredSessions) {
546
+ const staleSessionRef = ref(db, `${sessionPath}/${this.currentUser.uid}/sessions/${expiredSession.id}`);
547
+ await set(staleSessionRef, null);
548
+ this.emitEvent({
549
+ type: 'session_expired',
550
+ data: { session: expiredSession },
551
+ timestamp: Date.now(),
552
+ sessionId: expiredSession.id,
553
+ userId: expiredSession.userId
554
+ });
555
+ }
556
+ }
557
+ this._sessions = sessions;
558
+ }
559
+ else {
560
+ this._sessions = [];
561
+ }
562
+ }
563
+ catch (error) {
564
+ console.error('Failed to load sessions:', error);
565
+ }
566
+ }
567
+ // ========================================
568
+ // UTILITY METHODS
569
+ // ========================================
570
+ /**
571
+ * Get presence statistics
572
+ */
573
+ getPresenceStats() {
574
+ const sessions = this._sessions;
575
+ let totalSessionTime = 0;
576
+ const deviceSet = new Set();
577
+ let lastActivityTime = 0;
578
+ sessions.forEach((session) => {
579
+ if (session.device?.id) {
580
+ deviceSet.add(session.device.id);
581
+ }
582
+ const sessionStart = new Date(session.createdAt).getTime();
583
+ const sessionEnd = new Date(session.lastSeen).getTime();
584
+ totalSessionTime += sessionEnd - sessionStart;
585
+ if (session.lastActivity) {
586
+ const activityTime = new Date(session.lastActivity).getTime();
587
+ lastActivityTime = Math.max(lastActivityTime, activityTime);
588
+ }
589
+ });
590
+ const averageSessionDuration = sessions.length > 0 ? totalSessionTime / sessions.length : 0;
591
+ return {
592
+ totalSessions: sessions.length,
593
+ onlineSessions: sessions.filter((s) => s.status === 'online').length,
594
+ awaySessions: sessions.filter((s) => s.status === 'away').length,
595
+ offlineSessions: sessions.filter((s) => s.status === 'offline').length,
596
+ uniqueDevices: deviceSet.size,
597
+ averageSessionDuration,
598
+ lastActivity: lastActivityTime > 0 ? new Date(lastActivityTime).toISOString() : ''
599
+ };
600
+ }
601
+ /**
602
+ * Request location consent manually
603
+ */
604
+ async requestLocationConsent() {
605
+ if (!this.geolocationService) {
606
+ throw new PresenceError(PresenceErrorCode.GEOLOCATION_UNAVAILABLE, 'Geolocation service not available');
607
+ }
608
+ return await this.geolocationService.requestConsent();
609
+ }
610
+ /**
611
+ * Get current user's online sessions count
612
+ */
613
+ getOnlineSessionsCount() {
614
+ return this._sessions.filter((session) => session.status === 'online').length;
615
+ }
616
+ /**
617
+ * Check if user is online on any device
618
+ */
619
+ isUserOnline() {
620
+ return this._sessions.some((session) => session.status === 'online');
621
+ }
622
+ /**
623
+ * Get sessions by status
624
+ */
625
+ getSessionsByStatus(status) {
626
+ return this._sessions.filter((session) => session.status === status);
627
+ }
628
+ /**
629
+ * Force refresh presence data
630
+ */
631
+ async refresh() {
632
+ if (!this._initialized)
633
+ return;
634
+ this._loading = true;
635
+ try {
636
+ await this.loadSessions();
637
+ if (this._currentSession) {
638
+ await this.setPresence(this._status);
639
+ }
640
+ }
641
+ finally {
642
+ this._loading = false;
643
+ }
644
+ }
645
+ // ========================================
646
+ // EVENT MANAGEMENT
647
+ // ========================================
648
+ /**
649
+ * Add event listener
650
+ * @param callback Event callback function
651
+ * @returns Cleanup function to remove listener
652
+ */
653
+ addEventListener(callback) {
654
+ this.eventListeners.add(callback);
655
+ return () => this.eventListeners.delete(callback);
656
+ }
657
+ /**
658
+ * Emit event to all listeners
659
+ */
660
+ emitEvent(event) {
661
+ this.eventListeners.forEach((callback) => {
662
+ try {
663
+ callback(event);
664
+ }
665
+ catch (error) {
666
+ console.error('Error in presence event listener:', error);
667
+ }
668
+ });
669
+ }
670
+ // ========================================
671
+ // CLEANUP
672
+ // ========================================
673
+ /**
674
+ * Dispose of all resources and cleanup
675
+ */
676
+ async dispose() {
677
+ try {
678
+ // Set status to offline before cleanup
679
+ if (this._currentSession) {
680
+ await this.setPresence('offline');
681
+ }
682
+ }
683
+ catch (error) {
684
+ console.error('Error setting offline status during disposal:', error);
685
+ }
686
+ // Stop location tracking
687
+ this.geolocationService?.dispose();
688
+ // Remove connection listener
689
+ if (this.connectedListener) {
690
+ this.connectedListener();
691
+ this.connectedListener = null;
692
+ }
693
+ // Clear event listeners
694
+ this.eventListeners.clear();
695
+ // Reset state
696
+ this._initialized = false;
697
+ this._status = 'offline';
698
+ this._loading = false;
699
+ this._error = null;
700
+ this._currentSession = null;
701
+ this._sessions = [];
702
+ this.emitEvent({
703
+ type: 'disconnect',
704
+ timestamp: Date.now()
705
+ });
706
+ }
707
+ }
708
+ /**
709
+ * Pre-initialized singleton instance of FirekitPresence.
710
+ *
711
+ * @example
712
+ * ```typescript
713
+ * import { firekitPresence } from 'svelte-firekit';
714
+ *
715
+ * // Initialize presence tracking
716
+ * await firekitPresence.initialize(user, {
717
+ * geolocation: { enabled: true, type: 'browser' },
718
+ * sessionTTL: 30 * 60 * 1000
719
+ * });
720
+ *
721
+ * // Listen to events
722
+ * const unsubscribe = firekitPresence.addEventListener((event) => {
723
+ * console.log('Presence event:', event.type, event.data);
724
+ * });
725
+ * ```
726
+ */
727
+ export const firekitPresence = FirekitPresence.getInstance();