npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@littoral/literally-firebase

v0.5.0

Published

Firebase utilities and integration for littoral components.

Readme

@littoral/literally-firebase

Firebase Firestore integration utilities and reactive listener lifecycle mixins for Lit web components.

npm License: MIT


Installation

Install the package along with its peer dependencies:

npm install @littoral/literally-firebase @littoral/literally lit firebase

Features

Reactive Firestore Controllers (Recommended)

Lit Reactive Controllers provide declarative document and query subscriptions without deep mixin inheritance hierarchies. They automatically manage connection lifecycles (hostConnected/hostDisconnected), dynamic reference updates, and reactive states (data, loading, error, exists, count, isFromCache, hasPendingWrites).

FirestoreDocController

Subscribes to a single Firestore document. Supports static DocumentReference objects or reactive getter functions that re-evaluate when component properties change.

import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { doc, getFirestore } from 'firebase/firestore';
import { FirestoreDocController } from '@littoral/literally-firebase/firestore';
import { userConverter, type UserProfile } from './models';

@customElement('user-profile-card')
export class UserProfileCard extends LitElement {
  @property() userId!: string;

  private user = new FirestoreDocController<UserProfile>(this, {
    ref: () =>
      this.userId
        ? doc(getFirestore(), 'users', this.userId).withConverter(userConverter)
        : null,
  });

  render() {
    if (this.user.loading) return html`<p>Loading user data...</p>`;
    if (this.user.error) return html`<p>Error: ${this.user.error.message}</p>`;
    if (!this.user.exists) return html`<p>User not found.</p>`;

    return html`
      <div>
        <h3>${this.user.data?.name}</h3>
        <p>${this.user.data?.email}</p>
        ${this.user.isFromCache ? html`<small>(offline cache)</small>` : ''}
      </div>
    `;
  }
}

FirestoreQueryController

Subscribes to Firestore collections or queries with automatic mapping to typed arrays.

import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { collection, query, where, getFirestore } from 'firebase/firestore';
import { FirestoreQueryController } from '@littoral/literally-firebase/firestore';
import { taskConverter, type Task } from './models';

@customElement('task-list')
export class TaskList extends LitElement {
  @property({ type: Boolean }) completed = false;

  private tasks = new FirestoreQueryController<Task>(this, {
    query: () => {
      const col = collection(getFirestore(), 'tasks').withConverter(
        taskConverter,
      );
      return query(col, where('done', '==', this.completed));
    },
  });

  render() {
    if (this.tasks.loading) return html`<p>Loading tasks...</p>`;
    if (this.tasks.error)
      return html`<p>Error: ${this.tasks.error.message}</p>`;
    if (this.tasks.empty) return html`<p>No tasks found.</p>`;

    return html`
      <p>Count: ${this.tasks.count}</p>
      <ul>
        ${this.tasks.data.map((task) => html`<li>${task.title}</li>`)}
      </ul>
    `;
  }
}

FirestoreListenerMixin

A LitElement mixin that simplifies managing Firebase Firestore real-time snapshot listeners (onSnapshot).

  • Automatically tracks active unsubscribe functions by a unique key.
  • Deduplicates watchers: re-registering a watcher under an existing key automatically unsubscribes the previous listener first.
  • Prevents memory leaks by automatically unsubscribing all active listeners in the component's disconnectedCallback lifecycle hook.

Example Usage

import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { doc, onSnapshot, getFirestore } from 'firebase/firestore';
import { FirestoreListenerMixin } from '@littoral/literally-firebase/mixins/firestore-watchers.mixin';

@customElement('user-profile-card')
export class UserProfileCard extends FirestoreListenerMixin(LitElement) {
  @state() private userData: any = null;

  connectedCallback() {
    super.connectedCallback();
    this.subscribeToUser('user-123');
  }

  private subscribeToUser(userId: string) {
    const db = getFirestore();
    const userRef = doc(db, 'users', userId);

    // Register listener with a unique name
    const unsubscribe = onSnapshot(userRef, (snapshot) => {
      this.userData = snapshot.data();
    });

    // Automatically tracked and unsubscribed on disconnectedCallback()
    this.addFirebaseWatcher('user-subscription', unsubscribe);
  }

  render() {
    if (!this.userData) {
      return html`<p>Loading user data...</p>`;
    }

    return html`
      <div>
        <h3>${this.userData.name}</h3>
        <p>${this.userData.email}</p>
      </div>
    `;
  }
}

Mixin API

  • addFirebaseWatcher(name: string, watcher: Unsubscribe | undefined): void — Registers or replaces a listener by name.
  • hasFirebaseWatcher(name: string): boolean — Checks if a listener is currently registered.
  • stopFirebaseWatcher(name: string): void — Unsubscribes and cleans up a specific listener by name.
  • clearFirebaseWatchers(): void — Unsubscribes and clears all registered listeners.


Converter & Serialization Toolkit

@littoral/literally-firebase/firestore provides utilities to create type-safe converters, eliminate runtime undefined field errors, and streamline Date <-> Timestamp conversions.

createConverter<T>()

A factory function creating an FBConverter<T> (compatible with withConverter()) with automated document ID injection, date conversion, and undefined property removal.

import { createConverter } from '@littoral/literally-firebase/firestore';

export interface CampInfo {
  id: string;
  name: string;
  startOn: Date;
  endOn: Date;
  notes?: string;
  meta?: { lastModified?: Date };
}

export const campConverter = createConverter<CampInfo>({
  idField: 'id', // Injects snapshot.id on read (default: 'id')
  dateFields: ['startOn', 'endOn', 'meta.lastModified'], // Converts Date <-> Timestamp automatically
  cleanUndefined: true, // Strips undefined fields before writing (default: true)
});

cleanFirestoreData(data, options?)

Recursively removes undefined properties from an object or array before sending it to Firestore (e.g. via setDoc or updateDoc), preventing Unsupported field value: undefined crashes.

Preserves Firestore types (Timestamp, FieldValue, DocumentReference, GeoPoint, Bytes) and native Date instances intact.

import {
  updateDoc,
  doc,
  serverTimestamp,
  getFirestore,
} from 'firebase/firestore';
import { cleanFirestoreData } from '@littoral/literally-firebase/firestore';

const db = getFirestore();
const campRef = doc(db, 'camps', 'camp-123');

await updateDoc(
  campRef,
  cleanFirestoreData({
    notes: userNote || undefined, // undefined values safely stripped
    updatedAt: serverTimestamp(), // FieldValue sentinels preserved
  }),
);

Date Utilities: toDate & toTimestamp

import { toDate, toTimestamp } from '@littoral/literally-firebase/firestore';

const date = toDate(snapshot.get('createdOn')); // Date from Timestamp, ISO string, or number
const timestamp = toTimestamp(new Date()); // Firestore Timestamp from Date or string

Authentication (@littoral/literally-firebase/auth)

Reactive controller and helper functions for Firebase Authentication.

AuthController

A Reactive Controller tracking user, custom claims, and authentication loading state with automatic onIdTokenChanged listener management.

import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
import { getAuth } from 'firebase/auth';
import {
  AuthController,
  signInWithGoogleWithFallback,
} from '@littoral/literally-firebase/auth';

@customElement('user-menu')
export class UserMenu extends LitElement {
  private auth = new AuthController(this, { auth: getAuth() });

  private handleSignIn = async () => {
    await signInWithGoogleWithFallback(getAuth());
  };

  render() {
    if (this.auth.loading) return html`<p>Loading user...</p>`;

    if (!this.auth.isLoggedIn) {
      return html`<button @click=${this.handleSignIn}>
        Sign In with Google
      </button>`;
    }

    return html`
      <div>
        <span>Welcome, ${this.auth.displayName}</span>
        ${this.auth.hasClaim('admin') ? html`<span class="badge">Admin</span>` : ''}
        <button @click=${() => this.auth.signOut()}>Sign Out</button>
      </div>
    `;
  }
}

signInWithGoogleWithFallback

Attempts popup sign-in, automatically falling back to signInWithRedirect if popup blockers intercept the request on mobile Safari or iOS devices.

import { getAuth } from 'firebase/auth';
import { signInWithGoogleWithFallback } from '@littoral/literally-firebase/auth';

await signInWithGoogleWithFallback(getAuth());

Roadmap

See ROADMAP.md for planned features, reactive controllers, converter enhancements, and testing utilities.


License

MIT