svelte-firekit 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,8 @@ A complete Firebase integration library for **Svelte 5** and **SvelteKit**. Buil
7
7
  - SSR-safe — all services degrade gracefully on the server
8
8
  - Works without SvelteKit (`$app/*`, `$env/*` are never imported)
9
9
 
10
+ **Documentation:** [sveltefirekit.com](https://sveltefirekit.com/)
11
+
10
12
  ---
11
13
 
12
14
  ## Installation
@@ -162,28 +164,41 @@ try {
162
164
 
163
165
  ```ts
164
166
  // Wait for auth to initialize (useful for SSR / load functions)
165
- const user = await firekitUser.waitForAuth();
167
+ const user = await firekitUser.waitForAuth(); // 10s timeout (default)
168
+ const user = await firekitUser.waitForAuth(5_000); // custom timeout in ms
166
169
  ```
167
170
 
168
171
  ### Auth components
169
172
 
173
+ All auth components wait for Firebase Auth to initialize before rendering. Use the optional `fallback` snippet to show a loading state.
174
+
170
175
  ```svelte
171
- <SignedIn> <p>Only shown when signed in</p> </SignedIn>
172
- <SignedOut> <p>Only shown when signed out</p> </SignedOut>
176
+ <SignedIn>
177
+ {#snippet children(user)}<p>Welcome, {user.displayName}</p>{/snippet}
178
+ {#snippet fallback()}<p>Loading...</p>{/snippet}
179
+ </SignedIn>
180
+
181
+ <SignedOut>
182
+ {#snippet children(signIn)}<button onclick={signIn}>Sign in</button>{/snippet}
183
+ {#snippet fallback()}<p>Loading...</p>{/snippet}
184
+ </SignedOut>
173
185
 
174
186
  <!-- Route guard with redirect callback -->
175
187
  <AuthGuard requireAuth={true} onUnauthorized={() => goto('/login')}>
176
- <p>Protected content</p>
188
+ {#snippet children(user, signOut)}<p>Protected content</p>{/snippet}
189
+ {#snippet fallback()}<p>Loading...</p>{/snippet}
177
190
  </AuthGuard>
178
191
 
179
192
  <!-- Custom guard with async checks (e.g. role verification) -->
180
193
  <CustomGuard
181
194
  verificationChecks={[
182
- async () => { const doc = await getDoc(...); return doc.data()?.role === 'admin'; }
195
+ async (user) => user.emailVerified,
196
+ async (user) => { const doc = await getDoc(...); return doc.data()?.role === 'admin'; }
183
197
  ]}
184
198
  onUnauthorized={() => goto('/403')}
185
199
  >
186
- <p>Admin only</p>
200
+ {#snippet children(user, signOut)}<p>Admin only</p>{/snippet}
201
+ {#snippet fallback()}<p>Checking permissions...</p>{/snippet}
187
202
  </CustomGuard>
188
203
  ```
189
204
 
@@ -415,16 +430,44 @@ List files:
415
430
  {#each dir.items as item}<p>{item.name}</p>{/each}
416
431
  ```
417
432
 
418
- Using the `<DownloadURL>` and `<UploadTask>` components:
433
+ ### File upload validation
434
+
435
+ Validate files before uploading — checks size, MIME type, and image dimensions.
436
+
437
+ ```ts
438
+ import { validateFile } from 'svelte-firekit';
439
+
440
+ const result = await validateFile(file, {
441
+ maxSize: 5 * 1024 * 1024, // 5 MB
442
+ accept: ['image/png', 'image/jpeg', '.webp'],
443
+ maxWidth: 2048,
444
+ maxHeight: 2048,
445
+ minWidth: 100
446
+ });
447
+
448
+ if (!result.valid) {
449
+ result.errors.forEach((e) => console.log(e.code, e.message));
450
+ }
451
+ ```
452
+
453
+ The `<UploadTask>` component supports an optional `validate` prop and `invalid` snippet:
454
+
455
+ ```svelte
456
+ <UploadTask path="uploads/{file.name}" {file} validate={{ maxSize: 5_000_000, accept: ['image/*'] }}>
457
+ {#snippet uploading(task)}<progress value={task.progress} max={100} />{/snippet}
458
+ {#snippet complete(url)}<img src={url} alt="uploaded" />{/snippet}
459
+ {#snippet invalid(result)}
460
+ {#each result.errors as err}<p class="error">{err.message}</p>{/each}
461
+ {/snippet}
462
+ </UploadTask>
463
+ ```
464
+
465
+ Using the `<DownloadURL>` component:
419
466
 
420
467
  ```svelte
421
468
  <DownloadURL path="images/avatar.jpg">
422
469
  {#snippet data(url)}<img src={url} />{/snippet}
423
470
  </DownloadURL>
424
-
425
- <UploadTask path="uploads/file.jpg" {file}>
426
- {#snippet data({ progress, downloadURL })}<progress value={progress} max={100} />{/snippet}
427
- </UploadTask>
428
471
  ```
429
472
 
430
473
  ---
@@ -566,6 +609,44 @@ firekitInAppMessaging.unsuppress();
566
609
 
567
610
  ---
568
611
 
612
+ ## Network / Offline Status
613
+
614
+ Track browser connectivity and Firestore sync state reactively.
615
+
616
+ ```svelte
617
+ <script lang="ts">
618
+ import { firekitNetwork } from 'svelte-firekit';
619
+ </script>
620
+
621
+ {#if !firekitNetwork.online}
622
+ <p>You're offline. Changes will sync when reconnected.</p>
623
+ {:else if firekitNetwork.hasPendingWrites}
624
+ <p>Saving...</p>
625
+ {:else}
626
+ <p>All changes saved</p>
627
+ {/if}
628
+ ```
629
+
630
+ Using the `<NetworkStatus>` component:
631
+
632
+ ```svelte
633
+ <NetworkStatus>
634
+ {#snippet online()}<span class="green">Connected</span>{/snippet}
635
+ {#snippet offline()}<span class="red">Offline</span>{/snippet}
636
+ {#snippet pending()}<span>Saving...</span>{/snippet}
637
+ </NetworkStatus>
638
+ ```
639
+
640
+ Manual control:
641
+
642
+ ```ts
643
+ await firekitNetwork.goOffline(); // force offline mode
644
+ await firekitNetwork.goOnline(); // reconnect
645
+ firekitNetwork.trackWrite(); // mark a pending write
646
+ ```
647
+
648
+ ---
649
+
569
650
  ## Presence
570
651
 
571
652
  ```ts
@@ -656,6 +737,7 @@ const data = await posts.waitForReady();
656
737
  | `firekitAnalytics` | Analytics event logging |
657
738
  | `firekitMessaging` | Firebase Cloud Messaging |
658
739
  | `firekitInAppMessaging` | In-App Messaging suppression control |
740
+ | `firekitNetwork` | Reactive network/offline status |
659
741
  | `firekitAppCheck` | App Check initialization |
660
742
 
661
743
  ### Reactive classes
@@ -676,6 +758,12 @@ const data = await posts.waitForReady();
676
758
  | `FirekitCallable` / `firekitCallable()` | Typed Cloud Function caller |
677
759
  | `FirekitCallableFromURL` / `firekitCallableFromURL()` | Cloud Function by URL |
678
760
 
761
+ ### Utilities
762
+
763
+ | Import | Description |
764
+ |---|---|
765
+ | `validateFile()` | Pre-upload file validation (size, type, dimensions) |
766
+
679
767
  ### Components
680
768
 
681
769
  | Component | Description |
@@ -689,7 +777,8 @@ const data = await posts.waitForReady();
689
777
  | `<Collection>` | Reactive Firestore collection |
690
778
  | `<Node>` | Reactive RTDB node |
691
779
  | `<DownloadURL>` | Storage download URL |
692
- | `<UploadTask>` | Resumable file upload |
780
+ | `<UploadTask>` | Resumable file upload with optional validation |
781
+ | `<NetworkStatus>` | Network/sync status display |
693
782
 
694
783
  ---
695
784
 
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { firekitNetwork } from '../services/network.svelte.js';
4
+
5
+ /**
6
+ * Renders snippets based on network/sync status.
7
+ *
8
+ * @example
9
+ * ```svelte
10
+ * <NetworkStatus>
11
+ * {#snippet online()}<span class="dot green" />{/snippet}
12
+ * {#snippet offline()}<span class="dot red">Offline</span>{/snippet}
13
+ * {#snippet pending()}<span>Saving...</span>{/snippet}
14
+ * </NetworkStatus>
15
+ * ```
16
+ */
17
+ let {
18
+ online: onlineSnippet,
19
+ offline: offlineSnippet,
20
+ pending: pendingSnippet
21
+ }: {
22
+ /** Rendered when online and all writes are synced. */
23
+ online?: Snippet;
24
+ /** Rendered when the browser is offline. */
25
+ offline?: Snippet;
26
+ /** Rendered when there are pending Firestore writes. */
27
+ pending?: Snippet;
28
+ } = $props();
29
+ </script>
30
+
31
+ {#if !firekitNetwork.online && offlineSnippet}
32
+ {@render offlineSnippet()}
33
+ {:else if firekitNetwork.hasPendingWrites && pendingSnippet}
34
+ {@render pendingSnippet()}
35
+ {:else if firekitNetwork.online && onlineSnippet}
36
+ {@render onlineSnippet()}
37
+ {/if}
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** Rendered when online and all writes are synced. */
4
+ online?: Snippet;
5
+ /** Rendered when the browser is offline. */
6
+ offline?: Snippet;
7
+ /** Rendered when there are pending Firestore writes. */
8
+ pending?: Snippet;
9
+ };
10
+ declare const NetworkStatus: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type NetworkStatus = ReturnType<typeof NetworkStatus>;
12
+ export default NetworkStatus;
@@ -2,16 +2,20 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { UploadMetadata } from 'firebase/storage';
4
4
  import { FirekitUploadTask, type UploadState } from '../services/storage.svelte.js';
5
+ import { validateFile } from '../utils/validation.js';
6
+ import type { FileValidationOptions, FileValidationResult } from '../types/storage.js';
5
7
 
6
8
  /**
7
9
  * Manages a Firebase Storage upload with reactive progress.
8
- *
9
- * Exposes upload controls (pause, resume, cancel) and progress state
10
- * to the `uploading` snippet.
10
+ * Optionally validates the file before uploading.
11
11
  *
12
12
  * @example
13
13
  * ```svelte
14
- * <UploadTask path="uploads/{file.name}" {file}>
14
+ * <UploadTask
15
+ * path="uploads/{file.name}"
16
+ * {file}
17
+ * validate={{ maxSize: 5 * 1024 * 1024, accept: ['image/*'] }}
18
+ * >
15
19
  * {#snippet uploading(task)}
16
20
  * <progress value={task.progress} max={100} />
17
21
  * <button onclick={task.pause}>Pause</button>
@@ -19,6 +23,11 @@
19
23
  * {#snippet complete(url)}
20
24
  * <img src={url} alt="uploaded" />
21
25
  * {/snippet}
26
+ * {#snippet invalid(result)}
27
+ * {#each result.errors as err}
28
+ * <p class="error">{err.message}</p>
29
+ * {/each}
30
+ * {/snippet}
22
31
  * </UploadTask>
23
32
  * ```
24
33
  */
@@ -26,9 +35,11 @@
26
35
  path,
27
36
  file,
28
37
  metadata,
38
+ validate,
29
39
  uploading: uploadingSnippet,
30
40
  complete: completeSnippet,
31
- error: errorSnippet
41
+ error: errorSnippet,
42
+ invalid: invalidSnippet
32
43
  }: {
33
44
  /** Storage destination path. */
34
45
  path: string;
@@ -36,6 +47,8 @@
36
47
  file: Blob | Uint8Array | ArrayBuffer;
37
48
  /** Optional upload metadata (content type, custom metadata, etc.). */
38
49
  metadata?: UploadMetadata;
50
+ /** Optional validation rules. Only applied when `file` is a File instance. */
51
+ validate?: FileValidationOptions;
39
52
  /**
40
53
  * Rendered while the upload is active.
41
54
  * Receives an object with `{ progress, state, bytesTransferred, totalBytes, pause, resume, cancel }`.
@@ -51,25 +64,55 @@
51
64
  }]>;
52
65
  /** Rendered after a successful upload. Receives the download URL. */
53
66
  complete?: Snippet<[string]>;
54
- /** Rendered on error. */
67
+ /** Rendered on upload error. */
55
68
  error?: Snippet<[Error]>;
69
+ /** Rendered when validation fails. Receives the validation result with errors. */
70
+ invalid?: Snippet<[FileValidationResult]>;
56
71
  } = $props();
57
72
 
58
- const task = new FirekitUploadTask(path, file, metadata);
73
+ let validationResult = $state<FileValidationResult | null>(null);
74
+ let task = $state<FirekitUploadTask | null>(null);
75
+ let started = false;
76
+
77
+ // Run validation (if configured) then start upload.
78
+ // Uses $effect so Svelte properly tracks the prop references.
79
+ $effect(() => {
80
+ if (started) return;
81
+ started = true;
82
+
83
+ if (validate && file instanceof File) {
84
+ validateFile(file, validate).then((result) => {
85
+ validationResult = result;
86
+ if (result.valid) {
87
+ task = new FirekitUploadTask(path, file as Blob, metadata);
88
+ }
89
+ });
90
+ } else {
91
+ task = new FirekitUploadTask(path, file, metadata);
92
+ }
93
+ });
59
94
  </script>
60
95
 
61
- {#if task.error && errorSnippet}
62
- {@render errorSnippet(task.error)}
63
- {:else if task.completed && completeSnippet && task.downloadURL}
64
- {@render completeSnippet(task.downloadURL)}
65
- {:else if task.active && uploadingSnippet}
66
- {@render uploadingSnippet({
67
- progress: task.progress,
68
- state: task.state,
69
- bytesTransferred: task.bytesTransferred,
70
- totalBytes: task.totalBytes,
71
- pause: () => task.pause(),
72
- resume: () => task.resume(),
73
- cancel: () => task.cancel()
74
- })}
96
+ {#if validationResult && !validationResult.valid}
97
+ {#if invalidSnippet}
98
+ {@render invalidSnippet(validationResult)}
99
+ {:else if errorSnippet}
100
+ {@render errorSnippet(new Error(validationResult.errors.map((e) => e.message).join('; ')))}
101
+ {/if}
102
+ {:else if task}
103
+ {#if task.error && errorSnippet}
104
+ {@render errorSnippet(task.error)}
105
+ {:else if task.completed && completeSnippet && task.downloadURL}
106
+ {@render completeSnippet(task.downloadURL)}
107
+ {:else if task.active && uploadingSnippet}
108
+ {@render uploadingSnippet({
109
+ progress: task.progress,
110
+ state: task.state,
111
+ bytesTransferred: task.bytesTransferred,
112
+ totalBytes: task.totalBytes,
113
+ pause: () => task!.pause(),
114
+ resume: () => task!.resume(),
115
+ cancel: () => task!.cancel()
116
+ })}
117
+ {/if}
75
118
  {/if}
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { UploadMetadata } from 'firebase/storage';
3
3
  import { type UploadState } from '../services/storage.svelte.js';
4
+ import type { FileValidationOptions, FileValidationResult } from '../types/storage.js';
4
5
  type $$ComponentProps = {
5
6
  /** Storage destination path. */
6
7
  path: string;
@@ -8,6 +9,8 @@ type $$ComponentProps = {
8
9
  file: Blob | Uint8Array | ArrayBuffer;
9
10
  /** Optional upload metadata (content type, custom metadata, etc.). */
10
11
  metadata?: UploadMetadata;
12
+ /** Optional validation rules. Only applied when `file` is a File instance. */
13
+ validate?: FileValidationOptions;
11
14
  /**
12
15
  * Rendered while the upload is active.
13
16
  * Receives an object with `{ progress, state, bytesTransferred, totalBytes, pause, resume, cancel }`.
@@ -25,8 +28,10 @@ type $$ComponentProps = {
25
28
  ]>;
26
29
  /** Rendered after a successful upload. Receives the download URL. */
27
30
  complete?: Snippet<[string]>;
28
- /** Rendered on error. */
31
+ /** Rendered on upload error. */
29
32
  error?: Snippet<[Error]>;
33
+ /** Rendered when validation fails. Receives the validation result with errors. */
34
+ invalid?: Snippet<[FileValidationResult]>;
30
35
  };
31
36
  declare const UploadTask: import("svelte").Component<$$ComponentProps, {}, "">;
32
37
  type UploadTask = ReturnType<typeof UploadTask>;
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export { FirekitRemoteConfig, firekitRemoteConfig, type FirekitRemoteConfigOptio
20
20
  export { firekitPerformance, type PerformanceTrace } from './services/performance.js';
21
21
  export { firekitMessaging, type MessagePayload } from './services/messaging.svelte.js';
22
22
  export { firekitAnalytics } from './services/analytics.js';
23
+ export { firekitNetwork } from './services/network.svelte.js';
23
24
  export { loadFirestoreBundle, getNamedQuery } from './services/bundles.js';
24
25
  export { default as FirebaseApp } from './components/FirebaseApp.svelte';
25
26
  export { default as SignedIn } from './components/SignedIn.svelte';
@@ -31,5 +32,6 @@ export { default as Collection } from './components/Collection.svelte';
31
32
  export { default as Node } from './components/Node.svelte';
32
33
  export { default as DownloadURL } from './components/DownloadURL.svelte';
33
34
  export { default as UploadTask } from './components/UploadTask.svelte';
35
+ export { default as NetworkStatus } from './components/NetworkStatus.svelte';
34
36
  export * from './types/index.js';
35
37
  export * from './utils/index.js';
package/dist/index.js CHANGED
@@ -32,6 +32,8 @@ export { firekitPerformance } from './services/performance.js';
32
32
  export { firekitMessaging } from './services/messaging.svelte.js';
33
33
  // ─── Analytics ────────────────────────────────────────────────────────────────
34
34
  export { firekitAnalytics } from './services/analytics.js';
35
+ // ─── Network / Offline ───────────────────────────────────────────────────────
36
+ export { firekitNetwork } from './services/network.svelte.js';
35
37
  // ─── Firestore bundles ────────────────────────────────────────────────────────
36
38
  export { loadFirestoreBundle, getNamedQuery } from './services/bundles.js';
37
39
  // ─── Components ──────────────────────────────────────────────────────────────
@@ -45,6 +47,7 @@ export { default as Collection } from './components/Collection.svelte';
45
47
  export { default as Node } from './components/Node.svelte';
46
48
  export { default as DownloadURL } from './components/DownloadURL.svelte';
47
49
  export { default as UploadTask } from './components/UploadTask.svelte';
50
+ export { default as NetworkStatus } from './components/NetworkStatus.svelte';
48
51
  // ─── Types ───────────────────────────────────────────────────────────────────
49
52
  export * from './types/index.js';
50
53
  // ─── Utilities ───────────────────────────────────────────────────────────────
@@ -0,0 +1,71 @@
1
+ import { NetworkError } from '../types/network.js';
2
+ /**
3
+ * Reactive network/offline status store.
4
+ *
5
+ * Tracks browser connectivity, Firestore sync state, and pending writes.
6
+ * Provides manual control to go offline/online.
7
+ *
8
+ * @example
9
+ * ```svelte
10
+ * <script>
11
+ * import { firekitNetwork } from 'svelte-firekit';
12
+ * </script>
13
+ *
14
+ * {#if !firekitNetwork.online}
15
+ * <p>You're offline. Changes will sync when reconnected.</p>
16
+ * {/if}
17
+ *
18
+ * {#if firekitNetwork.hasPendingWrites}
19
+ * <p>Saving...</p>
20
+ * {:else}
21
+ * <p>All changes saved</p>
22
+ * {/if}
23
+ * ```
24
+ */
25
+ declare class FirekitNetwork {
26
+ private static instance;
27
+ private _online;
28
+ private _synced;
29
+ private _hasPendingWrites;
30
+ private _initialized;
31
+ private _error;
32
+ private _firestoreEnabled;
33
+ private _listening;
34
+ private _db;
35
+ private _syncUnsub;
36
+ private _onlineHandler;
37
+ private _offlineHandler;
38
+ private constructor();
39
+ static getInstance(): FirekitNetwork;
40
+ private ensureListening;
41
+ /**
42
+ * Called by FirebaseApp or lazily on first getter access.
43
+ * Safe to call multiple times.
44
+ */
45
+ initialize(): void;
46
+ /** Whether the browser has network connectivity. */
47
+ get online(): boolean;
48
+ /** Whether all Firestore snapshots are in sync with the server. */
49
+ get synced(): boolean;
50
+ /** Whether there are unconfirmed Firestore writes. */
51
+ get hasPendingWrites(): boolean;
52
+ /** Whether the network store has been initialized. */
53
+ get initialized(): boolean;
54
+ /** Last error from network operations. */
55
+ get error(): NetworkError | null;
56
+ /** Whether Firestore network is currently enabled. */
57
+ get firestoreEnabled(): boolean;
58
+ /**
59
+ * Call this when you perform a Firestore write to track pending state.
60
+ * Uses `waitForPendingWrites` to automatically clear when the write is acknowledged.
61
+ */
62
+ trackWrite(): void;
63
+ /** Enables Firestore network access (reconnects to server). */
64
+ goOnline(): Promise<void>;
65
+ /** Disables Firestore network access (forces offline mode). */
66
+ goOffline(): Promise<void>;
67
+ clearError(): void;
68
+ dispose(): void;
69
+ }
70
+ export declare const firekitNetwork: FirekitNetwork;
71
+ export {};
@@ -0,0 +1,167 @@
1
+ import { onSnapshotsInSync, enableNetwork, disableNetwork, waitForPendingWrites } from 'firebase/firestore';
2
+ import { firebaseService } from '../firebase.js';
3
+ import { NetworkError, NetworkErrorCode } from '../types/network.js';
4
+ /**
5
+ * Reactive network/offline status store.
6
+ *
7
+ * Tracks browser connectivity, Firestore sync state, and pending writes.
8
+ * Provides manual control to go offline/online.
9
+ *
10
+ * @example
11
+ * ```svelte
12
+ * <script>
13
+ * import { firekitNetwork } from 'svelte-firekit';
14
+ * </script>
15
+ *
16
+ * {#if !firekitNetwork.online}
17
+ * <p>You're offline. Changes will sync when reconnected.</p>
18
+ * {/if}
19
+ *
20
+ * {#if firekitNetwork.hasPendingWrites}
21
+ * <p>Saving...</p>
22
+ * {:else}
23
+ * <p>All changes saved</p>
24
+ * {/if}
25
+ * ```
26
+ */
27
+ class FirekitNetwork {
28
+ static instance;
29
+ // ── Reactive state ──────────────────────────────────────────────────────
30
+ _online = $state(typeof navigator !== 'undefined' ? navigator.onLine : true);
31
+ _synced = $state(true);
32
+ _hasPendingWrites = $state(false);
33
+ _initialized = $state(false);
34
+ _error = $state(null);
35
+ _firestoreEnabled = $state(true);
36
+ // ── Internal ────────────────────────────────────────────────────────────
37
+ _listening = false;
38
+ _db = null;
39
+ _syncUnsub = null;
40
+ _onlineHandler = null;
41
+ _offlineHandler = null;
42
+ constructor() { }
43
+ static getInstance() {
44
+ if (!FirekitNetwork.instance) {
45
+ FirekitNetwork.instance = new FirekitNetwork();
46
+ }
47
+ return FirekitNetwork.instance;
48
+ }
49
+ ensureListening() {
50
+ if (this._listening || typeof window === 'undefined')
51
+ return;
52
+ try {
53
+ const db = firebaseService.getDbInstance();
54
+ if (!db)
55
+ return;
56
+ this._listening = true;
57
+ // Browser online/offline events
58
+ this._onlineHandler = () => { this._online = true; };
59
+ this._offlineHandler = () => { this._online = false; };
60
+ window.addEventListener('online', this._onlineHandler);
61
+ window.addEventListener('offline', this._offlineHandler);
62
+ // Firestore sync state — fires when all active listeners are caught up
63
+ this._syncUnsub = onSnapshotsInSync(db, () => {
64
+ this._synced = true;
65
+ this._hasPendingWrites = false;
66
+ });
67
+ this._db = db;
68
+ this._initialized = true;
69
+ }
70
+ catch {
71
+ // Firestore not configured yet — will retry on next getter access
72
+ }
73
+ }
74
+ /**
75
+ * Called by FirebaseApp or lazily on first getter access.
76
+ * Safe to call multiple times.
77
+ */
78
+ initialize() {
79
+ this.ensureListening();
80
+ }
81
+ // ── Public getters (reactive) ───────────────────────────────────────────
82
+ /** Whether the browser has network connectivity. */
83
+ get online() { this.ensureListening(); return this._online; }
84
+ /** Whether all Firestore snapshots are in sync with the server. */
85
+ get synced() { this.ensureListening(); return this._synced; }
86
+ /** Whether there are unconfirmed Firestore writes. */
87
+ get hasPendingWrites() { this.ensureListening(); return this._hasPendingWrites; }
88
+ /** Whether the network store has been initialized. */
89
+ get initialized() { return this._initialized; }
90
+ /** Last error from network operations. */
91
+ get error() { return this._error; }
92
+ /** Whether Firestore network is currently enabled. */
93
+ get firestoreEnabled() { return this._firestoreEnabled; }
94
+ // ── Methods ─────────────────────────────────────────────────────────────
95
+ /**
96
+ * Call this when you perform a Firestore write to track pending state.
97
+ * Uses `waitForPendingWrites` to automatically clear when the write is acknowledged.
98
+ */
99
+ trackWrite() {
100
+ this._hasPendingWrites = true;
101
+ this._synced = false;
102
+ if (this._db) {
103
+ waitForPendingWrites(this._db)
104
+ .then(() => {
105
+ this._hasPendingWrites = false;
106
+ this._synced = true;
107
+ })
108
+ .catch(() => {
109
+ // Firestore may have been disabled
110
+ });
111
+ }
112
+ }
113
+ /** Enables Firestore network access (reconnects to server). */
114
+ async goOnline() {
115
+ try {
116
+ const db = firebaseService.getDbInstance();
117
+ if (!db)
118
+ throw new NetworkError(NetworkErrorCode.FIRESTORE_UNAVAILABLE, 'Firestore is not available.');
119
+ await enableNetwork(db);
120
+ this._firestoreEnabled = true;
121
+ this._error = null;
122
+ }
123
+ catch (err) {
124
+ if (err instanceof NetworkError)
125
+ throw err;
126
+ const error = new NetworkError(NetworkErrorCode.ENABLE_NETWORK_FAILED, 'Failed to enable network.', err);
127
+ this._error = error;
128
+ throw error;
129
+ }
130
+ }
131
+ /** Disables Firestore network access (forces offline mode). */
132
+ async goOffline() {
133
+ try {
134
+ const db = firebaseService.getDbInstance();
135
+ if (!db)
136
+ throw new NetworkError(NetworkErrorCode.FIRESTORE_UNAVAILABLE, 'Firestore is not available.');
137
+ await disableNetwork(db);
138
+ this._firestoreEnabled = false;
139
+ this._error = null;
140
+ }
141
+ catch (err) {
142
+ if (err instanceof NetworkError)
143
+ throw err;
144
+ const error = new NetworkError(NetworkErrorCode.DISABLE_NETWORK_FAILED, 'Failed to disable network.', err);
145
+ this._error = error;
146
+ throw error;
147
+ }
148
+ }
149
+ clearError() {
150
+ this._error = null;
151
+ }
152
+ dispose() {
153
+ this._listening = false;
154
+ this._syncUnsub?.();
155
+ this._syncUnsub = null;
156
+ if (this._onlineHandler) {
157
+ window.removeEventListener('online', this._onlineHandler);
158
+ this._onlineHandler = null;
159
+ }
160
+ if (this._offlineHandler) {
161
+ window.removeEventListener('offline', this._offlineHandler);
162
+ this._offlineHandler = null;
163
+ }
164
+ this._initialized = false;
165
+ }
166
+ }
167
+ export const firekitNetwork = FirekitNetwork.getInstance();
@@ -5,3 +5,5 @@ export * from './collection.js';
5
5
  export * from './mutations.js';
6
6
  export * from './presence.js';
7
7
  export * from './analytics.js';
8
+ export * from './storage.js';
9
+ export * from './network.js';
@@ -5,3 +5,5 @@ export * from './collection.js';
5
5
  export * from './mutations.js';
6
6
  export * from './presence.js';
7
7
  export * from './analytics.js';
8
+ export * from './storage.js';
9
+ export * from './network.js';
@@ -0,0 +1,10 @@
1
+ export declare enum NetworkErrorCode {
2
+ FIRESTORE_UNAVAILABLE = "network/firestore-unavailable",
3
+ ENABLE_NETWORK_FAILED = "network/enable-network-failed",
4
+ DISABLE_NETWORK_FAILED = "network/disable-network-failed"
5
+ }
6
+ export declare class NetworkError extends Error {
7
+ code: NetworkErrorCode;
8
+ originalError?: unknown | undefined;
9
+ constructor(code: NetworkErrorCode, message: string, originalError?: unknown | undefined);
10
+ }
@@ -0,0 +1,16 @@
1
+ export var NetworkErrorCode;
2
+ (function (NetworkErrorCode) {
3
+ NetworkErrorCode["FIRESTORE_UNAVAILABLE"] = "network/firestore-unavailable";
4
+ NetworkErrorCode["ENABLE_NETWORK_FAILED"] = "network/enable-network-failed";
5
+ NetworkErrorCode["DISABLE_NETWORK_FAILED"] = "network/disable-network-failed";
6
+ })(NetworkErrorCode || (NetworkErrorCode = {}));
7
+ export class NetworkError extends Error {
8
+ code;
9
+ originalError;
10
+ constructor(code, message, originalError) {
11
+ super(message);
12
+ this.code = code;
13
+ this.originalError = originalError;
14
+ this.name = 'NetworkError';
15
+ }
16
+ }
@@ -0,0 +1,34 @@
1
+ export interface FileValidationOptions {
2
+ /** Maximum file size in bytes. */
3
+ maxSize?: number;
4
+ /** Minimum file size in bytes. */
5
+ minSize?: number;
6
+ /** Allowed MIME types or extensions. Supports wildcards like `'image/*'` and extensions like `'.jpg'`. */
7
+ accept?: string[];
8
+ /** Maximum image width in pixels. Only checked for image files. */
9
+ maxWidth?: number;
10
+ /** Maximum image height in pixels. Only checked for image files. */
11
+ maxHeight?: number;
12
+ /** Minimum image width in pixels. Only checked for image files. */
13
+ minWidth?: number;
14
+ /** Minimum image height in pixels. Only checked for image files. */
15
+ minHeight?: number;
16
+ }
17
+ export declare enum FileValidationErrorCode {
18
+ FILE_TOO_LARGE = "validation/file-too-large",
19
+ FILE_TOO_SMALL = "validation/file-too-small",
20
+ INVALID_MIME_TYPE = "validation/invalid-mime-type",
21
+ IMAGE_TOO_WIDE = "validation/image-too-wide",
22
+ IMAGE_TOO_TALL = "validation/image-too-tall",
23
+ IMAGE_TOO_NARROW = "validation/image-too-narrow",
24
+ IMAGE_TOO_SHORT = "validation/image-too-short",
25
+ DIMENSION_CHECK_FAILED = "validation/dimension-check-failed"
26
+ }
27
+ export interface FileValidationError {
28
+ code: FileValidationErrorCode;
29
+ message: string;
30
+ }
31
+ export interface FileValidationResult {
32
+ valid: boolean;
33
+ errors: FileValidationError[];
34
+ }
@@ -0,0 +1,11 @@
1
+ export var FileValidationErrorCode;
2
+ (function (FileValidationErrorCode) {
3
+ FileValidationErrorCode["FILE_TOO_LARGE"] = "validation/file-too-large";
4
+ FileValidationErrorCode["FILE_TOO_SMALL"] = "validation/file-too-small";
5
+ FileValidationErrorCode["INVALID_MIME_TYPE"] = "validation/invalid-mime-type";
6
+ FileValidationErrorCode["IMAGE_TOO_WIDE"] = "validation/image-too-wide";
7
+ FileValidationErrorCode["IMAGE_TOO_TALL"] = "validation/image-too-tall";
8
+ FileValidationErrorCode["IMAGE_TOO_NARROW"] = "validation/image-too-narrow";
9
+ FileValidationErrorCode["IMAGE_TOO_SHORT"] = "validation/image-too-short";
10
+ FileValidationErrorCode["DIMENSION_CHECK_FAILED"] = "validation/dimension-check-failed";
11
+ })(FileValidationErrorCode || (FileValidationErrorCode = {}));
@@ -2,3 +2,4 @@ export * from './errors.js';
2
2
  export * from './providers.js';
3
3
  export * from './user.js';
4
4
  export * from './firestore.js';
5
+ export * from './validation.js';
@@ -2,3 +2,4 @@ export * from './errors.js';
2
2
  export * from './providers.js';
3
3
  export * from './user.js';
4
4
  export * from './firestore.js';
5
+ export * from './validation.js';
@@ -0,0 +1,20 @@
1
+ import { type FileValidationOptions, type FileValidationResult } from '../types/storage.js';
2
+ /**
3
+ * Validates a File against the given options.
4
+ * Returns a result with `valid: true` if all checks pass, or `valid: false` with an array of errors.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const result = await validateFile(file, {
9
+ * maxSize: 5 * 1024 * 1024,
10
+ * accept: ['image/png', 'image/jpeg', '.webp'],
11
+ * maxWidth: 2048,
12
+ * maxHeight: 2048
13
+ * });
14
+ *
15
+ * if (!result.valid) {
16
+ * console.log(result.errors);
17
+ * }
18
+ * ```
19
+ */
20
+ export declare function validateFile(file: File, options: FileValidationOptions): Promise<FileValidationResult>;
@@ -0,0 +1,147 @@
1
+ import { FileValidationErrorCode } from '../types/storage.js';
2
+ /** Common extension-to-MIME mappings for the `accept` option. */
3
+ const EXT_TO_MIME = {
4
+ '.jpg': 'image/jpeg',
5
+ '.jpeg': 'image/jpeg',
6
+ '.png': 'image/png',
7
+ '.gif': 'image/gif',
8
+ '.webp': 'image/webp',
9
+ '.svg': 'image/svg+xml',
10
+ '.bmp': 'image/bmp',
11
+ '.ico': 'image/x-icon',
12
+ '.avif': 'image/avif',
13
+ '.pdf': 'application/pdf',
14
+ '.mp4': 'video/mp4',
15
+ '.webm': 'video/webm',
16
+ '.mp3': 'audio/mpeg',
17
+ '.wav': 'audio/wav',
18
+ '.ogg': 'audio/ogg',
19
+ '.json': 'application/json',
20
+ '.csv': 'text/csv',
21
+ '.txt': 'text/plain',
22
+ '.zip': 'application/zip'
23
+ };
24
+ function formatBytes(bytes) {
25
+ if (bytes < 1024)
26
+ return `${bytes} B`;
27
+ if (bytes < 1024 * 1024)
28
+ return `${(bytes / 1024).toFixed(1)} KB`;
29
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
30
+ }
31
+ function matchesMime(fileType, fileName, pattern) {
32
+ // Extension pattern: '.jpg'
33
+ if (pattern.startsWith('.')) {
34
+ const expectedMime = EXT_TO_MIME[pattern.toLowerCase()];
35
+ if (expectedMime)
36
+ return fileType === expectedMime;
37
+ // Fallback: check the file name extension
38
+ return fileName.toLowerCase().endsWith(pattern.toLowerCase());
39
+ }
40
+ // Wildcard MIME: 'image/*'
41
+ if (pattern.endsWith('/*')) {
42
+ const prefix = pattern.slice(0, -1); // 'image/'
43
+ return fileType.startsWith(prefix);
44
+ }
45
+ // Exact MIME: 'image/png'
46
+ return fileType === pattern;
47
+ }
48
+ function getImageDimensions(file) {
49
+ return new Promise((resolve, reject) => {
50
+ const url = URL.createObjectURL(file);
51
+ const img = new Image();
52
+ img.onload = () => {
53
+ URL.revokeObjectURL(url);
54
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
55
+ };
56
+ img.onerror = () => {
57
+ URL.revokeObjectURL(url);
58
+ reject(new Error('Failed to load image for dimension check.'));
59
+ };
60
+ img.src = url;
61
+ });
62
+ }
63
+ /**
64
+ * Validates a File against the given options.
65
+ * Returns a result with `valid: true` if all checks pass, or `valid: false` with an array of errors.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const result = await validateFile(file, {
70
+ * maxSize: 5 * 1024 * 1024,
71
+ * accept: ['image/png', 'image/jpeg', '.webp'],
72
+ * maxWidth: 2048,
73
+ * maxHeight: 2048
74
+ * });
75
+ *
76
+ * if (!result.valid) {
77
+ * console.log(result.errors);
78
+ * }
79
+ * ```
80
+ */
81
+ export async function validateFile(file, options) {
82
+ const errors = [];
83
+ // ── Size checks ──────────────────────────────────────────────────────────
84
+ if (options.maxSize !== undefined && file.size > options.maxSize) {
85
+ errors.push({
86
+ code: FileValidationErrorCode.FILE_TOO_LARGE,
87
+ message: `File size ${formatBytes(file.size)} exceeds maximum ${formatBytes(options.maxSize)}.`
88
+ });
89
+ }
90
+ if (options.minSize !== undefined && file.size < options.minSize) {
91
+ errors.push({
92
+ code: FileValidationErrorCode.FILE_TOO_SMALL,
93
+ message: `File size ${formatBytes(file.size)} is below minimum ${formatBytes(options.minSize)}.`
94
+ });
95
+ }
96
+ // ── MIME type check ──────────────────────────────────────────────────────
97
+ if (options.accept && options.accept.length > 0) {
98
+ const accepted = options.accept.some((pattern) => matchesMime(file.type, file.name, pattern));
99
+ if (!accepted) {
100
+ errors.push({
101
+ code: FileValidationErrorCode.INVALID_MIME_TYPE,
102
+ message: `File type "${file.type || 'unknown'}" is not accepted. Allowed: ${options.accept.join(', ')}.`
103
+ });
104
+ }
105
+ }
106
+ // ── Image dimension checks ───────────────────────────────────────────────
107
+ const needsDimensions = options.maxWidth !== undefined ||
108
+ options.maxHeight !== undefined ||
109
+ options.minWidth !== undefined ||
110
+ options.minHeight !== undefined;
111
+ if (needsDimensions && file.type.startsWith('image/')) {
112
+ try {
113
+ const { width, height } = await getImageDimensions(file);
114
+ if (options.maxWidth !== undefined && width > options.maxWidth) {
115
+ errors.push({
116
+ code: FileValidationErrorCode.IMAGE_TOO_WIDE,
117
+ message: `Image width ${width}px exceeds maximum ${options.maxWidth}px.`
118
+ });
119
+ }
120
+ if (options.maxHeight !== undefined && height > options.maxHeight) {
121
+ errors.push({
122
+ code: FileValidationErrorCode.IMAGE_TOO_TALL,
123
+ message: `Image height ${height}px exceeds maximum ${options.maxHeight}px.`
124
+ });
125
+ }
126
+ if (options.minWidth !== undefined && width < options.minWidth) {
127
+ errors.push({
128
+ code: FileValidationErrorCode.IMAGE_TOO_NARROW,
129
+ message: `Image width ${width}px is below minimum ${options.minWidth}px.`
130
+ });
131
+ }
132
+ if (options.minHeight !== undefined && height < options.minHeight) {
133
+ errors.push({
134
+ code: FileValidationErrorCode.IMAGE_TOO_SHORT,
135
+ message: `Image height ${height}px is below minimum ${options.minHeight}px.`
136
+ });
137
+ }
138
+ }
139
+ catch {
140
+ errors.push({
141
+ code: FileValidationErrorCode.DIMENSION_CHECK_FAILED,
142
+ message: 'Could not read image dimensions for validation.'
143
+ });
144
+ }
145
+ }
146
+ return { valid: errors.length === 0, errors };
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-firekit",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "A Svelte library for Firebase integration",
5
5
  "license": "MIT",
6
6
  "author": "Giovani Rodriguez",