@littoral/literally-firebase 0.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0](https://github.com/Zoramite/literally/compare/literally-firebase-v0.1.0...literally-firebase-v0.2.0) (2026-06-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * Firestore watcher mixin and interface for firestore converter ([c51b2d2](https://github.com/Zoramite/literally/commit/c51b2d25a1f50ebd525b1d3a7222755e65a6f430))
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@littoral/literally-firebase",
3
+ "version": "0.2.0",
4
+ "description": "Firebase utilities and integration for littoral components.",
5
+ "exports": {
6
+ "./*": "./src/*.ts"
7
+ },
8
+ "keywords": [
9
+ "lit",
10
+ "firebase"
11
+ ],
12
+ "homepage": "https://github.com/Zoramite/literally#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/Zoramite/literally/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "Randy Merrill <Zoramite+github@gmail.com>",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Zoramite/literally.git"
21
+ },
22
+ "dependencies": {},
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "peerDependencies": {
27
+ "@littoral/literally": "^2.5.0",
28
+ "lit": "^3.3.3",
29
+ "firebase": "^12.14.0"
30
+ }
31
+ }
@@ -0,0 +1,35 @@
1
+ import {
2
+ QueryDocumentSnapshot,
3
+ type SnapshotOptions,
4
+ } from 'firebase/firestore';
5
+
6
+ /**
7
+ * Interface representing a Firestore data converter.
8
+ * Compatible with the custom object converter pattern expected by Firebase Firestore's `withConverter`.
9
+ *
10
+ * This allows you to write type-safe queries and database operations using strongly typed model classes/interfaces
11
+ * instead of raw Firestore DocumentData.
12
+ *
13
+ * @template Type The type representing the domain model class or interface.
14
+ */
15
+ export interface FBConverter<Type> {
16
+ /**
17
+ * Converts a domain model object of type `Type` into a plain object suitable for saving to Firestore.
18
+ *
19
+ * @param data The domain model instance to serialize.
20
+ * @returns A plain object suitable for writing to Firestore.
21
+ */
22
+ toFirestore: (data: Type) => any;
23
+
24
+ /**
25
+ * Converts a Firestore snapshot back into a strongly typed domain model object of type `Type`.
26
+ *
27
+ * @param snap The query document snapshot from Firestore.
28
+ * @param options Snapshot conversion options provided by the Firestore SDK.
29
+ * @returns The deserialized domain model instance.
30
+ */
31
+ fromFirestore: (
32
+ snap: QueryDocumentSnapshot,
33
+ options: SnapshotOptions,
34
+ ) => Type;
35
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @fileoverview Main entry point for the `@littoral/literally-firebase` package.
3
+ * Exports Firestore utilities and Lit element mixins for integration.
4
+ */
5
+
6
+ export { type FBConverter } from './firestore/converter';
7
+
8
+ export {
9
+ FirestoreListenerMixin,
10
+ type FirestoreListenerMixinInterface,
11
+ } from './mixins/firestore-watchers.mixin';
@@ -0,0 +1,58 @@
1
+ // @vitest-environment jsdom
2
+ import { fixture, defineCE } from '@open-wc/testing';
3
+ import { LitElement } from 'lit';
4
+ import { describe, test, expect, vi } from 'vitest';
5
+
6
+ import { FirestoreListenerMixin } from './firestore-watchers.mixin';
7
+
8
+ const TestTag = defineCE(class extends FirestoreListenerMixin(LitElement) {});
9
+
10
+ describe('FirestoreListenerMixin', () => {
11
+ test('adds and stops watchers', async () => {
12
+ const el = await fixture<any>(`<${TestTag}></${TestTag}>`);
13
+ const unsubscribe = vi.fn();
14
+
15
+ el.addFirebaseWatcher('test', unsubscribe);
16
+ expect(el.hasFirebaseWatcher('test')).toBe(true);
17
+
18
+ el.stopFirebaseWatcher('test');
19
+ expect(unsubscribe).toHaveBeenCalled();
20
+ expect(el.hasFirebaseWatcher('test')).toBe(false);
21
+ });
22
+
23
+ test('replaces existing watcher with same name', async () => {
24
+ const el = await fixture<any>(`<${TestTag}></${TestTag}>`);
25
+ const unsubscribe1 = vi.fn();
26
+ const unsubscribe2 = vi.fn();
27
+
28
+ el.addFirebaseWatcher('test', unsubscribe1);
29
+ el.addFirebaseWatcher('test', unsubscribe2);
30
+
31
+ expect(unsubscribe1).toHaveBeenCalled();
32
+ expect(el.hasFirebaseWatcher('test')).toBe(true);
33
+ });
34
+
35
+ test('stops all watchers on disconnect', async () => {
36
+ const el = await fixture<any>(`<${TestTag}></${TestTag}>`);
37
+ const unsubscribe1 = vi.fn();
38
+ const unsubscribe2 = vi.fn();
39
+
40
+ el.addFirebaseWatcher('one', unsubscribe1);
41
+ el.addFirebaseWatcher('two', unsubscribe2);
42
+
43
+ // Disconnect the element
44
+ el.remove();
45
+ // Wait for disconnectedCallback (it's synchronous but let's be safe)
46
+
47
+ expect(unsubscribe1).toHaveBeenCalled();
48
+ expect(unsubscribe2).toHaveBeenCalled();
49
+ expect(el.hasFirebaseWatcher('one')).toBe(false);
50
+ expect(el.hasFirebaseWatcher('two')).toBe(false);
51
+ });
52
+
53
+ test('handles undefined watcher in addFirebaseWatcher', async () => {
54
+ const el = await fixture<any>(`<${TestTag}></${TestTag}>`);
55
+ el.addFirebaseWatcher('test', undefined);
56
+ expect(el.hasFirebaseWatcher('test')).toBe(false);
57
+ });
58
+ });
@@ -0,0 +1,135 @@
1
+ import { type Constructor } from '@littoral/literally/mixins/mixin';
2
+ import { type Unsubscribe } from 'firebase/firestore';
3
+ import { LitElement } from 'lit';
4
+
5
+ /**
6
+ * Interface representing the API provided by `FirestoreListenerMixin`.
7
+ * Provides utilities to cleanly manage active Firestore live listeners (subscriptions)
8
+ * and ensure they are cleaned up when elements disconnect.
9
+ */
10
+ export interface FirestoreListenerMixinInterface {
11
+ /**
12
+ * Registers a active Firebase listener unsubscribe callback under a given name.
13
+ * If a watcher with the same name already exists, it is unsubscribed first before
14
+ * the new one is stored.
15
+ *
16
+ * @param name Unique name to identify the listener.
17
+ * @param watcher The unsubscribe callback returned by the Firestore listener configuration.
18
+ */
19
+ addFirebaseWatcher(name: string, watcher: Unsubscribe | undefined): void;
20
+
21
+ /**
22
+ * Checks whether a Firebase listener with the given name is currently active and registered.
23
+ *
24
+ * @param name Unique name of the listener.
25
+ * @returns True if the listener is active, false otherwise.
26
+ */
27
+ hasFirebaseWatcher(name: string): boolean;
28
+
29
+ /**
30
+ * Unsubscribes and stops a specific active listener by its unique name, removing it from the registry.
31
+ *
32
+ * @param name Unique name of the listener.
33
+ */
34
+ stopFirebaseWatcher(name: string): void;
35
+
36
+ /**
37
+ * Unsubscribes and stops all active listeners currently tracked by the mixin.
38
+ */
39
+ clearFirebaseWatchers(): void;
40
+ }
41
+
42
+ /**
43
+ * A LitElement mixin that simplifies managing Firestore reactive watch listeners.
44
+ *
45
+ * Automatically tracks unsubscribe functions returned by Firestore listener configuration
46
+ * (e.g. `onSnapshot`) and automatically cleans them up (unsubscribes) during the element's
47
+ * `disconnectedCallback` lifecycle hook to prevent memory leaks.
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * class MyElement extends FirestoreListenerMixin(LitElement) {
52
+ * connectedCallback() {
53
+ * super.connectedCallback();
54
+ * const unsubscribe = onSnapshot(docRef, (doc) => { ... });
55
+ * this.addFirebaseWatcher('my-doc-watcher', unsubscribe);
56
+ * }
57
+ * }
58
+ * ```
59
+ */
60
+ export const FirestoreListenerMixin = <T extends Constructor<LitElement>>(
61
+ superClass: T,
62
+ ) => {
63
+ class FirestoreListenerMixinElement
64
+ extends superClass
65
+ implements FirestoreListenerMixinInterface
66
+ {
67
+ static styles = [(superClass as unknown as typeof LitElement).styles ?? []];
68
+
69
+ /**
70
+ * Map tracking active Firestore unsubscribe callbacks by their registry key name.
71
+ */
72
+ protected firebaseWatchers: Record<string, Unsubscribe> = {};
73
+
74
+ /**
75
+ * Standard Web Component lifecycle hook.
76
+ * Invoked when the component is disconnected from the DOM.
77
+ * Automatically triggers cleanup of all registered Firebase listeners.
78
+ */
79
+ disconnectedCallback() {
80
+ this.clearFirebaseWatchers();
81
+ super.disconnectedCallback();
82
+ }
83
+
84
+ /**
85
+ * Iterates over all active registered Firebase watchers, calls their unsubscribe callbacks
86
+ * to terminate connection stream, and removes them from registry.
87
+ */
88
+ clearFirebaseWatchers() {
89
+ for (const key of Object.keys(this.firebaseWatchers)) {
90
+ this.firebaseWatchers[key]();
91
+ delete this.firebaseWatchers[key];
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Registers a new Firebase watcher unsubscribe callback under a given name.
97
+ * Ensures any previous watcher with the same name is cleanly terminated first.
98
+ *
99
+ * @param name Unique key for tracking this listener.
100
+ * @param watcher Unsubscribe callback returned by Firestore stream setups.
101
+ */
102
+ addFirebaseWatcher(name: string, watcher: Unsubscribe | undefined) {
103
+ // Stop any existing watchers with the same name.
104
+ this.firebaseWatchers[name]?.();
105
+
106
+ if (!watcher) {
107
+ return;
108
+ }
109
+
110
+ this.firebaseWatchers[name] = watcher;
111
+ }
112
+
113
+ /**
114
+ * Queries the registry to see if an active watcher is registered.
115
+ *
116
+ * @param name Unique key of the listener.
117
+ * @returns Boolean indicating presence of active listener.
118
+ */
119
+ hasFirebaseWatcher(name: string): boolean {
120
+ return Boolean(this.firebaseWatchers[name]);
121
+ }
122
+
123
+ /**
124
+ * Unsubscribes from a specific active watcher and deletes its registry key.
125
+ *
126
+ * @param name Unique key of the listener to stop.
127
+ */
128
+ stopFirebaseWatcher(name: string) {
129
+ this.firebaseWatchers[name]?.();
130
+ delete this.firebaseWatchers[name];
131
+ }
132
+ }
133
+ return FirestoreListenerMixinElement as Constructor<FirestoreListenerMixinInterface> &
134
+ T;
135
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src"],
8
+ "references": [{ "path": "../literally" }]
9
+ }