From 7acde1c1747459515a377ca8742bef70abf9b5ab Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Sun, 15 Aug 2021 10:45:26 +0530 Subject: [PATCH] feat: rest request sync with firestore --- helpers/fb/auth.ts | 36 ++++++++++++++++++- helpers/fb/request.ts | 83 +++++++++++++++++++++++++++++++++++++++++++ pages/index.vue | 27 ++++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 helpers/fb/request.ts diff --git a/helpers/fb/auth.ts b/helpers/fb/auth.ts index 6851d2dea..de51c5f36 100644 --- a/helpers/fb/auth.ts +++ b/helpers/fb/auth.ts @@ -1,7 +1,15 @@ import firebase from "firebase/app" import "firebase/firestore" import "firebase/auth" -import { BehaviorSubject, Subject } from "rxjs" +import { + BehaviorSubject, + distinctUntilChanged, + filter, + map, + Subject, + Subscription, +} from "rxjs" +import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api" export type HoppUser = firebase.User & { provider?: string @@ -219,3 +227,29 @@ export async function setProviderInfo(id: string, token: string) { throw e } } + +/** + * A Vue composable function that is called when the auth status + * is being updated to being logged in (fired multiple times), + * this is also called on component mount if the login + * was already resolved before mount. + */ +export function onLoggedIn(exec: (user: HoppUser) => void) { + let sub: Subscription | null = null + + onMounted(() => { + sub = currentUser$ + .pipe( + map((user) => !!user), // Get a logged in status (true or false) + distinctUntilChanged(), // Don't propagate unless the status updates + filter((x) => x) // Don't propagate unless it is logged in + ) + .subscribe(() => { + exec(currentUser$.value!) + }) + }) + + onBeforeUnmount(() => { + sub?.unsubscribe() + }) +} diff --git a/helpers/fb/request.ts b/helpers/fb/request.ts new file mode 100644 index 000000000..f56996e8a --- /dev/null +++ b/helpers/fb/request.ts @@ -0,0 +1,83 @@ +import firebase from "firebase/app" +import "firebase/firestore" +import { + audit, + combineLatest, + distinctUntilChanged, + EMPTY, + from, + map, + Subscription, +} from "rxjs" +import { + HoppRESTRequest, + translateToNewRequest, +} from "../types/HoppRESTRequest" +import { currentUser$, HoppUser } from "./auth" +import { restRequest$ } from "~/newstore/RESTSession" + +/** + * Writes a request to a user's firestore sync + * + * @param user The user to write to + * @param request The request to write to the request sync + */ +function writeCurrentRequest(user: HoppUser, request: HoppRESTRequest) { + return firebase + .firestore() + .collection("users") + .doc(user.uid) + .collection("requests") + .doc("rest") + .set(request) +} + +/** + * Loads the synced request from the firestore sync + * + * @returns Fetched request object if exists else null + */ +export async function loadRequestFromSync(): Promise { + const currentUser = currentUser$.value + + if (!currentUser) + throw new Error("Cannot load request from sync without login") + + const doc = await firebase + .firestore() + .collection("users") + .doc(currentUser.uid) + .collection("requests") + .doc("rest") + .get() + + const data = doc.data() + + if (!data) return null + else return translateToNewRequest(data) +} + +/** + * Performs sync of the REST Request session with Firestore. + * + * @returns A subscription to the sync observable stream. + * Unsubscribe to stop syncing. + */ +export function startRequestSync(): Subscription { + const sub = combineLatest([ + currentUser$, + restRequest$.pipe(distinctUntilChanged()), + ]) + .pipe( + map(([user, request]) => + user ? from(writeCurrentRequest(user, request)) : EMPTY + ), + audit((x) => x) + ) + .subscribe(() => { + // NOTE: This subscription should be kept + console.log("synced request") + }) + + return sub +} diff --git a/pages/index.vue b/pages/index.vue index 9f44dd8c8..13dead1f6 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -77,6 +77,7 @@ import { computed, defineComponent, + onBeforeUnmount, onMounted, useContext, watch, @@ -84,6 +85,7 @@ import { import { Splitpanes, Pane } from "splitpanes" import "splitpanes/dist/splitpanes.css" import { map } from "rxjs/operators" +import { Subscription } from "rxjs" import { useSetting } from "~/newstore/settings" import { restRequest$, @@ -101,6 +103,8 @@ import { useStream, useStreamSubscriber, } from "~/helpers/utils/composables" +import { loadRequestFromSync, startRequestSync } from "~/helpers/fb/request" +import { onLoggedIn } from "~/helpers/fb/auth" function bindRequestToURLParams() { const { @@ -159,11 +163,34 @@ function bindRequestToURLParams() { }) } +function setupRequestSync() { + const { route } = useContext() + + // Subscription to request sync + let sub: Subscription | null = null + + // Load request on login resolve and start sync + onLoggedIn(async () => { + if (Object.keys(route.value.query).length === 0) { + const request = await loadRequestFromSync() + if (request) setRESTRequest(request) + } + + sub = startRequestSync() + }) + + // Stop subscripton to stop syncing + onBeforeUnmount(() => { + sub?.unsubscribe() + }) +} + export default defineComponent({ components: { Splitpanes, Pane }, setup() { const { subscribeToStream } = useStreamSubscriber() + setupRequestSync() bindRequestToURLParams() subscribeToStream(restRequest$, (x) => {