Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- **[BREAKING]** `draftService.syncDrafts` to download draft data in the background
- `draftService.syncDrafts` to accept a priortyFn to prioritise specific draft downloads

### Fixed

Expand Down
155 changes: 101 additions & 54 deletions src/apps/draft-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ import {
ProgressListener,
} from './types/submissions'

export type PriorityFn = (
a: SubmissionTypes.FormSubmissionDraft,
b: SubmissionTypes.FormSubmissionDraft,
) => number

const DRAFT_CHUNK_SIZE = 5

function generateDraftsKey(username: string) {
return `V2_DRAFTS_${username}`
}
Expand Down Expand Up @@ -132,10 +139,15 @@ async function generatePublicLocalFormSubmissionDraftsFromStorage(
})
}

async function generateLocalFormSubmissionDraftsFromStorage(
localDraftsStorage: LocalDraftsStorage,
abortSignal: AbortSignal | undefined,
): Promise<LocalFormSubmissionDraft[]> {
async function generateLocalFormSubmissionDraftsFromStorage({
localDraftsStorage,
abortSignal,
priorityFn,
}: {
localDraftsStorage: LocalDraftsStorage
abortSignal: AbortSignal | undefined
priorityFn?: PriorityFn
}): Promise<LocalFormSubmissionDraft[]> {
const pendingSubmissionsDraftIds = await getPendingSubmissionsDraftIds()
const deletedDraftIds = new Set(
localDraftsStorage.deletedFormSubmissionDrafts.map(({ id }) => id),
Expand Down Expand Up @@ -183,51 +195,60 @@ async function generateLocalFormSubmissionDraftsFromStorage(

await broadcastUpdate()

// TODO Batch the downloads instead of sequentially
if (draftsToDownload.length) {
for (const formSubmissionDraft of draftsToDownload) {
const currentValue = localFormSubmissionDraftsMap.get(
formSubmissionDraft.id,
)
if (currentValue) {
localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...currentValue,
downloadStatus: 'DOWNLOADING',
})
}
await broadcastUpdate()

try {
const draftSubmission = await getDraftSubmission(
formSubmissionDraft,
abortSignal,
if (priorityFn) {
draftsToDownload.sort(priorityFn)
}
for (let i = 0; i < draftsToDownload.length; i += DRAFT_CHUNK_SIZE) {
const chunk = draftsToDownload.slice(i, i + DRAFT_CHUNK_SIZE)
chunk.forEach(async (formSubmissionDraft) => {
const currentValue = localFormSubmissionDraftsMap.get(
formSubmissionDraft.id,
)
if (draftSubmission) {
localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
draftSubmission: draftSubmission,
downloadStatus: 'SUCCESS',
})
} else {
if (currentValue) {
localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
downloadStatus: 'NOT_AVAILABLE',
...currentValue,
downloadStatus: 'DOWNLOADING',
})
}
} catch (err) {
console.warn(
`Could not fetch draft submission for draft: ${formSubmissionDraft.id}`,
err,
)
})
await broadcastUpdate()
// download the draft data for each draft in the chunk in parallel
await Promise.all(
chunk.map(async (formSubmissionDraft) => {
try {
const draftSubmission = await getDraftSubmission(
formSubmissionDraft,
abortSignal,
)
if (draftSubmission) {
localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
draftSubmission: draftSubmission,
downloadStatus: 'SUCCESS',
})
} else {
localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
downloadStatus: 'NOT_AVAILABLE',
})
}
} catch (err) {
console.warn(
`Could not fetch draft submission for draft: ${formSubmissionDraft.id}`,
err,
)

localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
downloadStatus: 'ERROR',
downloadError: (err as Error).message,
})
}
localFormSubmissionDraftsMap.set(formSubmissionDraft.id, {
...formSubmissionDraft,
downloadStatus: 'ERROR',
downloadError: (err as Error).message,
})
}

await broadcastUpdate()
await broadcastUpdate()
}),
)
}
}

Expand Down Expand Up @@ -430,7 +451,11 @@ async function upsertDraft({
}
}

await setAndBroadcastDrafts(localDraftsStorage, abortSignal)
await setAndBroadcastDrafts({
localDraftsStorage,
abortSignal,
priorityFn: undefined,
})
} else {
let updated = false
const publicDraftsStorage = (await getPublicDraftsFromStorage()).map(
Expand Down Expand Up @@ -520,10 +545,11 @@ async function getDrafts(
abortSignal?: AbortSignal,
): Promise<LocalFormSubmissionDraft[]> {
const localDraftsStorage = await getLocalDraftsFromStorage()
return await generateLocalFormSubmissionDraftsFromStorage(
return await generateLocalFormSubmissionDraftsFromStorage({
localDraftsStorage,
abortSignal,
)
priorityFn: undefined,
})
}

/**
Expand Down Expand Up @@ -590,7 +616,11 @@ async function getDraftAndData(
const localDraftsStorage = await getLocalDraftsFromStorage()
if (formSubmissionDrafts) {
localDraftsStorage.syncedFormSubmissionDrafts = formSubmissionDrafts
await setAndBroadcastDrafts(localDraftsStorage, abortSignal)
await setAndBroadcastDrafts({
localDraftsStorage,
abortSignal,
priorityFn: undefined,
})
} else {
formSubmissionDrafts = localDraftsStorage.syncedFormSubmissionDrafts
}
Expand Down Expand Up @@ -688,7 +718,11 @@ async function deleteDraft(
)
}

await setAndBroadcastDrafts(localDraftsStorage, abortSignal)
await setAndBroadcastDrafts({
localDraftsStorage,
abortSignal,
priorityFn: undefined,
})
} else {
let publicDraftsStorage = await getPublicDraftsFromStorage()
const draftSubmission = publicDraftsStorage.find(
Expand Down Expand Up @@ -731,10 +765,15 @@ async function setAndBroadcastPublicDrafts(publicDrafts: DraftSubmission[]) {
)
}

async function setAndBroadcastDrafts(
localDraftsStorage: LocalDraftsStorage,
abortSignal: AbortSignal | undefined,
): Promise<void> {
async function setAndBroadcastDrafts({
localDraftsStorage,
abortSignal,
priorityFn,
}: {
localDraftsStorage: LocalDraftsStorage
abortSignal: AbortSignal | undefined
priorityFn: PriorityFn | undefined
}): Promise<void> {
const username = getUsername()
if (!username) {
throw new OneBlinkAppsError(
Expand All @@ -751,10 +790,11 @@ async function setAndBroadcastDrafts(

console.log('Drafts have been updated', localDraftsStorage)
const localFormSubmissionDrafts =
await generateLocalFormSubmissionDraftsFromStorage(
await generateLocalFormSubmissionDraftsFromStorage({
localDraftsStorage,
abortSignal,
)
priorityFn,
})
await executeDraftsListeners(localFormSubmissionDrafts)
}

Expand All @@ -778,12 +818,19 @@ let _isSyncingDrafts = false
* @returns
*/
async function syncDrafts({
priorityFn,
formsAppId,
throwError,
abortSignal,
}: {
/** The id of the OneBlink Forms App to sync drafts with */
formsAppId: number
/**
* Function used to determine the order of the draft downloads. It is expected
* to return a negative value if the first argument is less than the second
* argument, zero if they're equal, and a positive value otherwise.
*/
priorityFn?: PriorityFn
/** `true` to throw errors while syncing */
throwError?: boolean
/** Signal to abort the requests */
Expand Down Expand Up @@ -884,7 +931,7 @@ async function syncDrafts({
}

console.log('Downloading drafts in the background')
setAndBroadcastDrafts(localDraftsStorage, abortSignal)
setAndBroadcastDrafts({ localDraftsStorage, abortSignal, priorityFn })
.then(async () => {
console.log('Finished syncing drafts.')
})
Expand Down
23 changes: 18 additions & 5 deletions src/hooks/useDrafts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { draftService, submissionService } from '../apps'
import useAuth from './useAuth'
import useIsMounted from './useIsMounted'
import useIsOffline from './useIsOffline'
import { PriorityFn } from '../apps/draft-service'

/** The value returned from `useDrafts()` hook */
export type DraftsContextValue = {
Expand All @@ -18,7 +19,13 @@ export type DraftsContextValue = {
*/
lastSyncTime: Date | null
/** A function to trigger syncing of the drafts */
syncDrafts: (abortSignal: AbortSignal | undefined) => Promise<void>
syncDrafts: ({
abortSignal,
priorityFn,
}: {
abortSignal: AbortSignal | undefined
priorityFn: PriorityFn | undefined
}) => Promise<void>
/** An Error object if syncing drafts fails */
syncError: Error | null
/** A function to clear Error object from syncing drafts */
Expand Down Expand Up @@ -100,9 +107,14 @@ export function DraftsContextProvider({
syncError: null,
}))
}, [])
// TODO add formId to prioritize downloads for specific forms
const syncDrafts = React.useCallback(
async (abortSignal: AbortSignal | undefined) => {
async ({
abortSignal,
priorityFn,
}: {
abortSignal: AbortSignal | undefined
priorityFn: PriorityFn | undefined
}) => {
if (!isDraftsEnabled || isUsingFormsKey) {
return
}
Expand All @@ -120,6 +132,7 @@ export function DraftsContextProvider({
try {
await draftService.syncDrafts({
formsAppId,
priorityFn,
throwError: true,
abortSignal,
})
Expand Down Expand Up @@ -178,7 +191,7 @@ export function DraftsContextProvider({
const abortController = new AbortController()
const unregisterPendingQueueListener =
submissionService.registerPendingQueueListener(() =>
syncDrafts(undefined),
syncDrafts({ abortSignal: undefined, priorityFn: undefined }),
)
const unregisterDraftsListener =
draftService.registerDraftsListener(setDrafts)
Expand All @@ -192,7 +205,7 @@ export function DraftsContextProvider({
React.useEffect(() => {
if (!isOffline) {
const abortController = new AbortController()
syncDrafts(abortController.signal)
syncDrafts({ abortSignal: abortController.signal, priorityFn: undefined })
return () => {
abortController.abort()
}
Expand Down