fix: update document flow fetch logic (#1039)

## Description

**Fixes issues with mismatching state between document steps.**

For example, editing a recipient and proceeding to the next step may not
display the updated recipient. And going back will display the old
recipient instead of the updated values.

**This PR also improves mutation and query speeds by adding logic to
bypass query invalidation.**

```ts
export const trpc = createTRPCReact<AppRouter>({
  unstable_overrides: {
    useMutation: {
      async onSuccess(opts) {
        await opts.originalFn();

        // This forces mutations to wait for all the queries on the page to reload, and in
        // this case one of the queries is `searchDocument` for the command overlay, which
        // on average takes ~500ms. This means that every single mutation must wait for this.
        await opts.queryClient.invalidateQueries(); 
      },
    },
  },
});
```

I've added workarounds to allow us to bypass things such as batching and
invalidating queries. But I think we should instead remove this and
update all the mutations where a query is required for a more optimised
system.

## Example benchmarks

Using stg-app vs this preview there's an average 50% speed increase
across mutations.

**Set signer step:**
Average old speed: ~1100ms
Average new speed: ~550ms

**Set recipient step:**
Average old speed: ~1200ms
Average new speed: ~600ms

**Set fields step:**
Average old speed: ~1200ms
Average new speed: ~600ms

## Related Issue

This will resolve #470

## Changes Made

- Added ability to skip batch queries
- Added a state to store the required document data.
- Refetch the data between steps if/when required
- Optimise mutations and queries

## Checklist

- [X] I have tested these changes locally and they work as expected.
- [X] I have followed the project's coding style guidelines.

---------

Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
This commit is contained in:
David Nguyen
2024-03-26 21:12:41 +08:00
committed by GitHub
parent 5210fe2963
commit 006b732edb
17 changed files with 279 additions and 81 deletions

View File

@ -0,0 +1,25 @@
/**
* For TRPC useQueries that should not be batched with other queries.
*/
export const SKIP_QUERY_BATCH_META = {
trpc: {
context: {
skipBatch: true,
},
},
};
/**
* For TRPC useQueries and useMutations to adjust the logic on when query invalidation
* should occur.
*
* When used in:
* - useQuery: Will not invalidate the given query when a mutation occurs.
* - useMutation: Will not trigger invalidation on all queries when mutation succeeds.
*
*/
export const DO_NOT_INVALIDATE_QUERY_ON_MUTATION = {
meta: {
doNotInvalidateQueryOnMutation: true,
},
};

View File

@ -0,0 +1,32 @@
import { prisma } from '@documenso/prisma';
import type { DocumentWithDetails } from '@documenso/prisma/types/document';
import { getDocumentWhereInput } from './get-document-by-id';
export type GetDocumentWithDetailsByIdOptions = {
id: number;
userId: number;
teamId?: number;
};
export const getDocumentWithDetailsById = async ({
id,
userId,
teamId,
}: GetDocumentWithDetailsByIdOptions): Promise<DocumentWithDetails> => {
const documentWhereInput = await getDocumentWhereInput({
documentId: id,
userId,
teamId,
});
return await prisma.document.findFirstOrThrow({
where: documentWhereInput,
include: {
documentData: true,
documentMeta: true,
Recipient: true,
Field: true,
},
});
};

View File

@ -5,7 +5,7 @@ import {
diffFieldChanges,
} from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { FieldType } from '@documenso/prisma/client';
import type { Field, FieldType } from '@documenso/prisma/client';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
export interface SetFieldsForDocumentOptions {
@ -29,7 +29,7 @@ export const setFieldsForDocument = async ({
documentId,
fields,
requestMetadata,
}: SetFieldsForDocumentOptions) => {
}: SetFieldsForDocumentOptions): Promise<Field[]> => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
@ -99,7 +99,7 @@ export const setFieldsForDocument = async ({
});
const persistedFields = await prisma.$transaction(async (tx) => {
await Promise.all(
return await Promise.all(
linkedFields.map(async (field) => {
const fieldSignerEmail = field.signerEmail.toLowerCase();
@ -218,5 +218,13 @@ export const setFieldsForDocument = async ({
});
}
return persistedFields;
// Filter out fields that have been removed or have been updated.
const filteredFields = existingFields.filter((field) => {
const isRemoved = removedFields.find((removedField) => removedField.id === field.id);
const isUpdated = persistedFields.find((persistedField) => persistedField.id === field.id);
return !isRemoved && !isUpdated;
});
return [...filteredFields, ...persistedFields];
};

View File

@ -6,6 +6,7 @@ import {
diffRecipientChanges,
} from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma';
import type { Recipient } from '@documenso/prisma/client';
import { RecipientRole } from '@documenso/prisma/client';
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
@ -28,7 +29,7 @@ export const setRecipientsForDocument = async ({
documentId,
recipients,
requestMetadata,
}: SetRecipientsForDocumentOptions) => {
}: SetRecipientsForDocumentOptions): Promise<Recipient[]> => {
const document = await prisma.document.findFirst({
where: {
id: documentId,
@ -226,5 +227,17 @@ export const setRecipientsForDocument = async ({
});
}
return persistedRecipients;
// Filter out recipients that have been removed or have been updated.
const filteredRecipients: Recipient[] = existingRecipients.filter((recipient) => {
const isRemoved = removedRecipients.find(
(removedRecipient) => removedRecipient.id === recipient.id,
);
const isUpdated = persistedRecipients.find(
(persistedRecipient) => persistedRecipient.id === recipient.id,
);
return !isRemoved && !isUpdated;
});
return [...filteredRecipients, ...persistedRecipients];
};

View File

@ -1,4 +1,10 @@
import { Document, Recipient } from '@documenso/prisma/client';
import type {
Document,
DocumentData,
DocumentMeta,
Field,
Recipient,
} from '@documenso/prisma/client';
export type DocumentWithRecipientAndSender = Omit<Document, 'document'> & {
recipient: Recipient;
@ -10,3 +16,10 @@ export type DocumentWithRecipientAndSender = Omit<Document, 'document'> & {
subject: string;
description: string;
};
export type DocumentWithDetails = Document & {
documentData: DocumentData;
documentMeta: DocumentMeta | null;
Recipient: Recipient[];
Field: Field[];
};

View File

@ -1,16 +1,22 @@
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import { createTRPCProxyClient, httpBatchLink, httpLink, splitLink } from '@trpc/client';
import SuperJSON from 'superjson';
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
import { AppRouter } from '../server/router';
import type { AppRouter } from '../server/router';
export const trpc = createTRPCProxyClient<AppRouter>({
transformer: SuperJSON,
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
splitLink({
condition: (op) => op.context.skipBatch === true,
true: httpLink({
url: `${getBaseUrl()}/api/trpc`,
}),
false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
}),
],
});

View File

@ -4,7 +4,7 @@ import { useState } from 'react';
import type { QueryClientConfig } from '@tanstack/react-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { httpBatchLink, httpLink, splitLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import SuperJSON from 'superjson';
@ -12,12 +12,22 @@ import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
import type { AppRouter } from '../server/router';
export { getQueryKey } from '@trpc/react-query';
export const trpc = createTRPCReact<AppRouter>({
unstable_overrides: {
overrides: {
useMutation: {
async onSuccess(opts) {
await opts.originalFn();
await opts.queryClient.invalidateQueries();
if (opts.meta.doNotInvalidateQueryOnMutation) {
return;
}
// Invalidate all queries besides ones that specify not to in the meta data.
await opts.queryClient.invalidateQueries({
predicate: (query) => !query?.meta?.doNotInvalidateQueryOnMutation,
});
},
},
},
@ -55,8 +65,14 @@ export function TrpcProvider({ children }: TrpcProviderProps) {
transformer: SuperJSON,
links: [
httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
splitLink({
condition: (op) => op.context.skipBatch === true,
true: httpLink({
url: `${getBaseUrl()}/api/trpc`,
}),
false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`,
}),
}),
],
}),

View File

@ -9,6 +9,7 @@ import { duplicateDocumentById } from '@documenso/lib/server-only/document/dupli
import { findDocumentAuditLogs } from '@documenso/lib/server-only/document/find-document-audit-logs';
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getDocumentWithDetailsById } from '@documenso/lib/server-only/document/get-document-with-details-by-id';
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { searchDocumentsWithKeyword } from '@documenso/lib/server-only/document/search-documents-with-keyword';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
@ -23,6 +24,7 @@ import {
ZFindDocumentAuditLogsQuerySchema,
ZGetDocumentByIdQuerySchema,
ZGetDocumentByTokenQuerySchema,
ZGetDocumentWithDetailsByIdQuerySchema,
ZResendDocumentMutationSchema,
ZSearchDocumentsMutationSchema,
ZSendDocumentMutationSchema,
@ -66,6 +68,24 @@ export const documentRouter = router({
}
}),
getDocumentWithDetailsById: authenticatedProcedure
.input(ZGetDocumentWithDetailsByIdQuerySchema)
.query(async ({ input, ctx }) => {
try {
return await getDocumentWithDetailsById({
...input,
userId: ctx.user.id,
});
} catch (err) {
console.error(err);
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'We were unable to find this document. Please try again later.',
});
}
}),
createDocument: authenticatedProcedure
.input(ZCreateDocumentMutationSchema)
.mutation(async ({ input, ctx }) => {

View File

@ -29,6 +29,15 @@ export const ZGetDocumentByTokenQuerySchema = z.object({
export type TGetDocumentByTokenQuerySchema = z.infer<typeof ZGetDocumentByTokenQuerySchema>;
export const ZGetDocumentWithDetailsByIdQuerySchema = z.object({
id: z.number().min(1),
teamId: z.number().min(1).optional(),
});
export type TGetDocumentWithDetailsByIdQuerySchema = z.infer<
typeof ZGetDocumentWithDetailsByIdQuerySchema
>;
export const ZCreateDocumentMutationSchema = z.object({
title: z.string().min(1),
documentDataId: z.string().min(1),