Skip to content

Commit

Permalink
feat: incremental-collection-store
Browse files Browse the repository at this point in the history
  • Loading branch information
jacob-8 committed Mar 19, 2024
1 parent 6f114e1 commit 31e0a50
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 47 deletions.
2 changes: 1 addition & 1 deletion src/lib/auth/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const authState = writable<User>(undefined, (set) => {

/**
* Subscribes to current Firebase user, pulls their data from the users collection, caches it to local storage as well as sets a cookie to allow for server-side rendering (not authenticated routes, just basic UI stuff like a name in a header). It also denotes their visit as a `lastVisit` timestamp in Firestore. */
export function createUserStore<T>(options: { userKey?: string; log?: boolean; startWith?: T }) {
export function createUserStore<T>(options: { userKey?: string; log?: boolean; startWith?: T } = {}) {
const { userKey = `${firebaseConfig.projectId}_firebase_user`, log = false, startWith = null } = options;

const { subscribe, set } = writable<T>(startWith);
Expand Down
3 changes: 3 additions & 0 deletions src/lib/firestore/stores.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// backwards compatibility in case anyone directly imported stores from here
export { docStore } from './stores/doc-store';
export { collectionStore } from './stores/collection-store';
30 changes: 11 additions & 19 deletions src/lib/firestore/stores/collection-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,16 @@ import { startTrace, stopTrace } from '../perf';
export function collectionStore<T>(
path: CollectionReference<T> | string,
queryConstraints: QueryConstraint[] = [],
opts: {
options: {
log?: boolean;
traceId?: string;
startWith?: T[];
maxWait?: number;
once?: boolean;
idField?: string;
refField?: string;
} = {
maxWait: 10000,
}
) {
const { startWith, log, traceId, maxWait, once, idField, refField } = {
idField: 'id',
...opts,
};
} = {}) {
const { startWith, log, traceId, maxWait = 10000, once, idField = 'id', refField } = options;

if (typeof window === 'undefined') {
const store = writable(startWith);
Expand All @@ -46,14 +41,15 @@ export function collectionStore<T>(
};
}

const { subscribe, set } = writable(startWith, start);

const ref = typeof path === 'string' ? colRef<T>(path) : path;
const q = query(ref, ...queryConstraints);
const trace = traceId && startTrace(traceId);

let _loading = typeof startWith !== undefined;
let _error = null;
let _meta = { first: null, last: null };
let _teardown;
let _waitForIt;

// Metadata for result
Expand All @@ -72,15 +68,15 @@ export function collectionStore<T>(
trace && stopTrace(trace);
};

const start = () => {
function start() {
_waitForIt =
maxWait &&
setTimeout(
() => _loading && next(null, new Error(`Timeout at ${maxWait}. Using fallback slot.`)),
() => _loading && next(null, new Error(`Timeout at ${maxWait}.`)),
maxWait
);

_teardown = onSnapshot(
const teardown = onSnapshot(
q,
(snapshot) => {
// Will always return an array
Expand All @@ -99,21 +95,17 @@ export function collectionStore<T>(
console.groupEnd();
}
next(data);
once && _teardown();
once && teardown();
},

(error) => {
console.error(error);
next(null, error);
}
);

return () => _teardown();
return () => teardown();
};

const store = writable(startWith, start);
const { subscribe, set } = store;

return {
subscribe,
ref,
Expand Down
44 changes: 18 additions & 26 deletions src/lib/firestore/stores/doc-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ import { startTrace, stopTrace } from '../perf';

export function docStore<T>(
path: DocumentReference<T> | string,
opts: { log?: boolean; traceId?: string; startWith?: T; maxWait?: number; once?: boolean } = {}
options: {
log?: boolean;
traceId?: string;
startWith?: T;
maxWait?: number;
once?: boolean
} = {}
) {
const { startWith, log, traceId, maxWait, once } = opts;
const { startWith, log, traceId, maxWait, once } = options;

if (typeof window === 'undefined') {
const store = writable<T>(startWith);
Expand All @@ -28,13 +34,14 @@ export function docStore<T>(
};
}

const { subscribe, set } = writable(startWith, start);

const ref = typeof path === 'string' ? docRef<T>(path) : path;
const trace = traceId && startTrace(traceId);

let _loading = typeof startWith !== undefined;
let _firstValue = true;
let _error = null;
let _teardown;
let _waitForIt;

// State should never change without emitting a new value
Expand All @@ -48,56 +55,41 @@ export function docStore<T>(
trace && stopTrace(trace);
};

// Timeout
// Runs of first subscription
const start = () => {
// Timeout for fallback slot
function start() {
_waitForIt =
maxWait &&
setTimeout(
() => _loading && next(null, new Error(`Timeout at ${maxWait}. Using fallback slot.`)),
() => _loading && next(null, new Error(`Timeout at ${maxWait}.`)),
maxWait
);

// Realtime firebase subscription
_teardown = onSnapshot(
const teardown = onSnapshot(
ref,
(snapshot) => {
const data = snapshot.data() || (_firstValue && startWith) || null;
if (data) {
// @ts-ignore
const data: T & { id?: string } = snapshot.data() || (_firstValue && startWith) || null;
if (data)
data.id = snapshot.id;
}
// Optional logging

if (log) {
console.groupCollapsed(`Doc ${snapshot.id}`);
console.log(`Path: ${ref.path}`);
console.table(data);
console.groupEnd();
}

// Emit next value
next(data);

// Teardown after first emitted value if once
once && _teardown();
once && teardown();
},

// Handle firebase thrown errors
(error) => {
console.error(error);
next(null, error);
}
);

// Removes firebase listener when store completes
return () => _teardown();
return () => teardown();
};

// Svelte store
const store = writable(startWith, start);
const { subscribe, set } = store;

return {
subscribe,
ref,
Expand Down
90 changes: 90 additions & 0 deletions src/lib/firestore/stores/incremental-collection-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { readable, writable } from 'svelte/store';
import {
onSnapshot,
query,
type CollectionReference,
type QueryConstraint,
} from 'firebase/firestore';

import { colRef, getCollection } from '../firestore';
import { startTrace, stopTrace } from '../perf';

export async function incrementalCollectionStore<T>(
path: CollectionReference<T> | string,
options: {
initialQueryConstraints: QueryConstraint[],
realtimeQueryConstraints?: QueryConstraint[],
log?: boolean;
traceId?: string;
maxWait?: number;
once?: boolean;
idField?: string;
refField?: string;
}) {
const { initialQueryConstraints = [], realtimeQueryConstraints = [], log, traceId, maxWait = 10000, once, idField = 'id', refField } = options;

const ref = typeof path === 'string' ? colRef<T>(path) : path;
const initial_collection = await getCollection<T>(ref, initialQueryConstraints);

if (typeof window === 'undefined') {
const { subscribe } = readable(initial_collection);
return {
subscribe,
ref,
get error() {
return false;
},
};
}

const { subscribe, set } = writable(initial_collection, start);
const q = query(ref, ...realtimeQueryConstraints);
const trace = traceId && startTrace(traceId);
const start_realtime_ms = Date.now();
let _error = null;

function start() {
const teardown = onSnapshot(
q,
(snapshot) => {
const data = snapshot.docs.map((docSnap) => ({
...docSnap.data(),
...(idField ? { [idField]: docSnap.id } : null),
...(refField ? { [refField]: docSnap.ref } : null),
}));

if (log) {
const type = 'Updated Query';
console.groupCollapsed(`${type} ${ref.id} | ${data.length} hits`);
console.log(`Path: ${ref.path}`);
console.table(data);
console.groupEnd();
}
next(data);
once && teardown();
},
(error) => {
console.error(error);
next(null, error);
}
);

return () => teardown();
};

function next(val: T[], err?: Error) {
_error = err || null;
if (initial_collection.length === val.length && Date.now() - start_realtime_ms < 100)
return console.log('ignoring next');
set(val);
trace && stopTrace(trace);
};

return {
subscribe,
ref,
get error() {
return _error;
},
};
}
3 changes: 2 additions & 1 deletion src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ export { default as Doc } from './firestore/Doc.svelte';
export { default as FirebaseUiAuth } from './auth/FirebaseUiAuth.svelte';

// Stores & Auth
export { collectionStore } from './firestore/stores/collection-store';
export { docStore } from './firestore/stores/doc-store';
export { collectionStore } from './firestore/stores/collection-store';
export { incrementalCollectionStore } from './firestore/stores/incremental-collection-store';
export { authState, createUserStore, logOut } from './auth/user';
export { updateUserData, saveUserData } from './auth/updateUserData';

Expand Down

0 comments on commit 31e0a50

Please sign in to comment.