Skip to content

Commit

Permalink
update GUI when new data arrives
Browse files Browse the repository at this point in the history
Now when you click "sync with Google Sheets", if you already have data
in Google Sheets, data immediately starts showing up on the page and you
get redirected to your first ledger.

This does expose a data syncing bug: it seems that all entries within a
ledger do not show up right away, but require several page refreshes to
actually retrieve. Yikes.
  • Loading branch information
chadoh committed Apr 24, 2023
1 parent f424a4e commit 536c720
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 12 deletions.
15 changes: 13 additions & 2 deletions src/data/backends/findOrCreateWorker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DATA_UPDATED_EVENT, DATA_UPDATED_EVENT_KEY } from '../local'

if (!window.Worker) {
throw new Error('WebWorkers not supported 😞 \n\nGet a better browser, friend!')
}
Expand All @@ -14,14 +16,23 @@ if (!window.Worker) {
*
* @param type must match one of the folders in `src/data/backends`, such as `google`
*/
export async function findOrCreateWorker(type: string): Promise<Worker> {
export async function findOrCreateWorker(
type: string,
onMessage: (e: MessageEvent<any>) => Promise<void>
): Promise<Worker> {
window.workers = window.workers ?? {}
if (window.workers[type]) return window.workers[type]!

const worker = new Worker(new URL('./' + type + '/worker.ts', import.meta.url), {
type: 'module',
})
worker.onmessage = async e => {
if (e.data === DATA_UPDATED_EVENT_KEY) {
window.dispatchEvent(DATA_UPDATED_EVENT)
}
await onMessage(e)
}
window.workers[type] = worker
worker.postMessage('sync')
return worker
}
}
5 changes: 2 additions & 3 deletions src/data/backends/google/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,12 @@ export async function load(): Promise<gapiAndTokenClient> {
}

async function initWorker() {
const worker = await findOrCreateWorker('google')
worker.onmessage = async (e) => {
const worker = await findOrCreateWorker('google', async e => {
if (e.data === 'authError') {
await refreshAccessToken()
worker.postMessage('sync')
}
}
})
}

export async function signIn(callback?: () => void) {
Expand Down
32 changes: 32 additions & 0 deletions src/data/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,34 @@ function syncWorkers() {
)
}

/**
* Subscribe to this key in a window.addEventListener to be notified of all data changes
*/
export const DATA_UPDATED_EVENT_KEY = 'data-updated'

/**
* Event to dispatch every time data changes
*/
export const DATA_UPDATED_EVENT = new Event(DATA_UPDATED_EVENT_KEY)


/**
* Set a value to the local data store and post `sync` message to all `window.workers`
*
* Turn off syncing by passing `sync: false`
*
* Broadcasts a {@type DATA_UPDATED_EVENT} when data changes, accounting for
* possibility that `set` is invoked from a worker (see related logic in
* `findOrCreateWorker`), so that subscribers can stay up-to-date.
*/
async function set<T>(key: string, value: T, sync = true): Promise<T> {
const ret = await store.setItem(key, value)
if (sync) syncWorkers()
if (typeof window === 'undefined') {
postMessage(DATA_UPDATED_EVENT_KEY)
} else {
window.dispatchEvent(DATA_UPDATED_EVENT)
}
return ret
}

Expand Down Expand Up @@ -259,3 +279,15 @@ async function clearOldestRecentDeletion() {
await store.setItem(RECENTLY_DELETED, current.slice(1))
}
}

/**
* Subscribe to all data changes, calling `onChange` each time
*
* Returns a function which will unsubscribe.
*/
export function subscribe(onChange: () => void): () => void {
window.addEventListener(DATA_UPDATED_EVENT_KEY, onChange)
return function unsubscribe() {
window.removeEventListener(DATA_UPDATED_EVENT_KEY, onChange)
}
}
4 changes: 2 additions & 2 deletions src/react/components/login.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { google, findOrCreateWorker } from '../../data'
import { google } from '../../data'

export default function Login() {
const [wantsGoogle, setWantsGoogle] = React.useState(false)
Expand Down Expand Up @@ -50,4 +50,4 @@ export default function Login() {
Sync with Google Sheets
</button>
)
}
}
14 changes: 14 additions & 0 deletions src/react/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React from 'react'
import { useNavigate } from 'react-router-dom'
import { subscribe } from '../data/local'

export function usePrev<T>(item: T): undefined | T {
const ref = React.useRef<T>()
Expand All @@ -7,3 +9,15 @@ export function usePrev<T>(item: T): undefined | T {
}, [item])
return ref.current
}

/**
* Refresh data from this route's `loader` when data changes in the local data store.
*/
export function useDataSubscription() {
const navigate = useNavigate()
React.useEffect(() => {
return subscribe(() => {
navigate('.', { replace: true })
})
}, [])
}
4 changes: 3 additions & 1 deletion src/react/routes/ledgers/entries/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import { useLoaderData, Form, Outlet, NavLink } from 'react-router-dom'
import type { LoaderFunction } from '@remix-run/router'
import { get, Entry } from '../../../../data/local'
import { useDataSubscription } from '../../../hooks'

export const loader: LoaderFunction = async ({ params }): Promise<Entry[]> => {
const name = params.ledgerName as string
Expand All @@ -16,6 +17,7 @@ export const loader: LoaderFunction = async ({ params }): Promise<Entry[]> => {
}

function Entries() {
useDataSubscription()
const entries = useLoaderData() as Entry[]
return (
<>
Expand Down Expand Up @@ -67,4 +69,4 @@ function Entries() {
)
}

export const element = <Entries />
export const element = <Entries />
4 changes: 3 additions & 1 deletion src/react/routes/ledgers/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import { useLoaderData, redirect } from 'react-router-dom'
import type { ActionFunction, LoaderFunction } from '@remix-run/router'
import { addLedger, getLedgers } from '../../../data/local'
import { useDataSubscription } from '../../hooks'
import Form from './form'

export const loader: LoaderFunction = async ({ request }): Promise<string[] | Response> => {
Expand All @@ -20,8 +21,9 @@ export const action: ActionFunction = async ({ request }) => {
}

function New() {
useDataSubscription()
const ledgers = useLoaderData() as Awaited<string[]>
return <Form ledgers={ledgers} />
}

export const element = <New />
export const element = <New />
6 changes: 4 additions & 2 deletions src/react/routes/ledgers/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {
import type { LoaderFunction } from '@remix-run/router'
import { getLedgers } from '../../../data/local'
import Login from '../../components/login'
import { useDataSubscription } from '../../hooks'

export const loader: LoaderFunction = async (): Promise<string[]> => {
return getLedgers()
}

function App() {
function Root() {
useDataSubscription()
const ledgers = useLoaderData() as Awaited<string[]>
const params = useParams()
return (
Expand Down Expand Up @@ -66,4 +68,4 @@ function App() {
)
}

export const element = <App />
export const element = <Root />
2 changes: 1 addition & 1 deletion src/react/routes/ledgers/show.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ function Show() {
)
}

export const element = <Show />
export const element = <Show />

0 comments on commit 536c720

Please sign in to comment.