Skip to content

Instantly share code, notes, and snippets.

@mosioc
Created May 26, 2026 09:29
Show Gist options
  • Select an option

  • Save mosioc/3644d45f02079be494e6215ea1e4ad9c to your computer and use it in GitHub Desktop.

Select an option

Save mosioc/3644d45f02079be494e6215ea1e4ad9c to your computer and use it in GitHub Desktop.
Large-scale offline-first app with useLiveQuery + state management
type Listener = () => void;
type Unsubscribe = () => void;
interface CacheEntry<T> {
data: T;
listeners: Set<Listener>;
dbUnsubscribe: Unsubscribe | null;
subscriberCount: number;
}
/**
* Normalize SQL and stringify params to produce a stable cache key.
*/
export function hashQuery(sql: string, params: unknown[]): string {
const normalizedSQL = sql.replace(/\s+/g, " ").trim();
return `${normalizedSQL}::${JSON.stringify(params)}`;
}
/**
* Returns true if the two values are meaningfully different.
* Bails early on reference equality, then does a shallow key/value comparison.
*/
function hasChanged<T>(prev: T, next: T): boolean {
if (prev === next) return false;
if (typeof prev !== "object" || typeof next !== "object") {
return prev !== next;
}
if (prev === null || next === null) return prev !== next;
const prevRecord = prev as Record<string, unknown>;
const nextRecord = next as Record<string, unknown>;
const prevKeys = Object.keys(prevRecord);
const nextKeys = Object.keys(nextRecord);
if (prevKeys.length !== nextKeys.length) return true;
for (const key of prevKeys) {
if (
!Object.prototype.hasOwnProperty.call(nextRecord, key) ||
prevRecord[key] !== nextRecord[key]
) {
return true;
}
}
return false;
}
export class QueryCache {
private cache = new Map<string, CacheEntry<unknown>>();
/**
* Subscribe to a cached live query.
*
* @param key - hashed query identifier
* @param setup - called once per cache entry to start the DB listener.
* Receives onData to call with fresh results.
* Must return a teardown function.
* @param onStoreChange - React's callback to notify of external store changes
* @returns a function that unsubscribes this specific listener
*/
subscribe<T>(
key: string,
setup: (onData: (data: T) => void) => Unsubscribe,
onStoreChange: Listener
): Unsubscribe {
let entry = this.cache.get(key) as CacheEntry<T> | undefined;
if (!entry) {
entry = {
data: undefined as unknown as T,
listeners: new Set(),
dbUnsubscribe: null,
subscriberCount: 0,
};
const handleData = (newData: T) => {
// Only notify if data actually changed
if (hasChanged(entry!.data, newData)) {
entry!.data = newData;
entry!.listeners.forEach((listener) => listener());
}
};
entry.dbUnsubscribe = setup(handleData);
this.cache.set(key, entry);
}
entry.listeners.add(onStoreChange);
entry.subscriberCount += 1;
// Return an unsubscribe function that only removes THIS listener
return () => {
const current = this.cache.get(key) as CacheEntry<T> | undefined;
if (!current) return;
current.listeners.delete(onStoreChange);
current.subscriberCount -= 1;
// If no more listeners, tear down the DB listener and remove the entry
if (current.subscriberCount === 0) {
current.dbUnsubscribe?.();
this.cache.delete(key);
}
};
}
/**
* Get the current cached data for a key. Returns undefined if not yet loaded.
*/
getSnapshot<T>(key: string): T | undefined {
const entry = this.cache.get(key);
return entry?.data as T | undefined;
}
/**
* Check if a cache entry exists and has active subscribers.
*/
hasActiveSubscribers(key: string): boolean {
const entry = this.cache.get(key);
return (entry?.subscriberCount ?? 0) > 0;
}
/**
* Force-invalidate a cached query, tearing down its listener.
* Next subscription will re-setup the DB listener.
*/
invalidate(key: string): void {
const entry = this.cache.get(key);
if (!entry) return;
entry.dbUnsubscribe?.();
this.cache.delete(key);
}
/**
* Invalidate all cached queries.
*/
clear(): void {
this.cache.forEach((entry) => {
entry.dbUnsubscribe?.();
});
this.cache.clear();
}
}
@mosioc

mosioc commented May 26, 2026

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment