Compare commits

...

9 Commits

Author SHA1 Message Date
1e6292b1d9 v2.0.4 2025-11-08 13:58:11 +11:00
d65866156d fix: remove parallel steps (#2152) 2025-11-08 13:57:26 +11:00
fe8915162f v2.0.3 2025-11-08 12:53:50 +11:00
37a2634aca feat: support optimizeParallelism for inngest jobs (#2151) 2025-11-08 12:53:13 +11:00
eff7d90f43 v2.0.2 2025-11-08 00:48:31 +11:00
db5524f8ce fix: resolve issue with sealing task on inngest (#2146)
Currently on inngest the sealing task fails during decoration stating
that it can not find the step "xxx"

My running theory is that this was due to it being a
Promise.all(map(...)) even though that isn't explicitly disallowed.

This change turns it into a for loop collecting promises to be awaited
after the fact.

Local inngest testing looks promising.
2025-11-08 00:48:13 +11:00
3d539b20ad v2.0.1 2025-11-07 23:42:03 +11:00
48626b9169 fix: support utf8 filenames download (#2145) 2025-11-07 23:41:31 +11:00
88371b665a fix: set correct envelope item cache url (#2144) 2025-11-07 16:50:58 +11:00
14 changed files with 131 additions and 57 deletions

View File

@ -41,6 +41,7 @@
"@simplewebauthn/server": "^9.0.3",
"autoprefixer": "^10.4.13",
"colord": "^2.9.3",
"content-disposition": "^0.5.4",
"framer-motion": "^10.12.8",
"hono": "4.7.0",
"hono-rate-limiter": "^0.4.2",
@ -87,6 +88,7 @@
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.1.2",
"@simplewebauthn/types": "^9.0.1",
"@types/content-disposition": "^0.5.9",
"@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1",
"@types/node": "^20",
@ -104,5 +106,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "2.0.0"
"version": "2.0.4"
}

View File

@ -1,4 +1,5 @@
import { type DocumentDataType, DocumentStatus } from '@prisma/client';
import contentDisposition from 'content-disposition';
import { type Context } from 'hono';
import { sha256 } from '@documenso/lib/universal/crypto';
@ -34,7 +35,7 @@ export const handleEnvelopeItemFileRequest = async ({
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
if (c.req.header('If-None-Match') === etag) {
if (c.req.header('If-None-Match') === etag && !isDownload) {
return c.body(null, 304);
}
@ -58,8 +59,7 @@ export const handleEnvelopeItemFileRequest = async ({
if (status === DocumentStatus.COMPLETED) {
c.header('Cache-Control', 'public, max-age=31536000, immutable');
} else {
// Set a tiny 1 minute cache, with must-revalidate to ensure the client always checks for updates.
c.header('Cache-Control', 'public, max-age=60, must-revalidate');
c.header('Cache-Control', 'public, max-age=0, must-revalidate');
}
}
@ -69,7 +69,7 @@ export const handleEnvelopeItemFileRequest = async ({
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
const filename = `${baseTitle}${suffix}`;
c.header('Content-Disposition', `attachment; filename="${filename}"`);
c.header('Content-Disposition', contentDisposition(filename));
// For downloads, prevent caching to ensure fresh data
c.header('Cache-Control', 'no-cache, no-store, must-revalidate');

15
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "2.0.0",
"version": "2.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "2.0.0",
"version": "2.0.4",
"workspaces": [
"apps/*",
"packages/*"
@ -100,7 +100,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "2.0.0",
"version": "2.0.4",
"dependencies": {
"@cantoo/pdf-lib": "^2.5.2",
"@documenso/api": "*",
@ -129,6 +129,7 @@
"@simplewebauthn/server": "^9.0.3",
"autoprefixer": "^10.4.13",
"colord": "^2.9.3",
"content-disposition": "^0.5.4",
"framer-motion": "^10.12.8",
"hono": "4.7.0",
"hono-rate-limiter": "^0.4.2",
@ -175,6 +176,7 @@
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-typescript": "^12.1.2",
"@simplewebauthn/types": "^9.0.1",
"@types/content-disposition": "^0.5.9",
"@types/formidable": "^2.0.6",
"@types/luxon": "^3.3.1",
"@types/node": "^20",
@ -12315,6 +12317,13 @@
"@types/node": "*"
}
},
"node_modules/@types/content-disposition": {
"version": "0.5.9",
"resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz",
"integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/cross-spawn": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz",

View File

@ -1,6 +1,6 @@
{
"private": true,
"version": "2.0.0",
"version": "2.0.4",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --filter=@documenso/remix",

View File

@ -25,7 +25,7 @@ import { DocumentStatus } from '@prisma/client';
import fs from 'node:fs';
import path from 'node:path';
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma';
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
import { seedUser } from '@documenso/prisma/seed/users';
@ -94,7 +94,8 @@ test.skip('field placement visual regression', async ({ page }, testInfo) => {
await Promise.all(
completedDocument.envelopeItems.map(async (item) => {
const documentUrl = getEnvelopeDownloadUrl({
const documentUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: item,
token,
version: 'signed',
@ -179,7 +180,8 @@ test.skip('download envelope images', async ({ page }) => {
await Promise.all(
completedDocument.envelopeItems.map(async (item) => {
const documentUrl = getEnvelopeDownloadUrl({
const documentUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: item,
token,
version: 'signed',

View File

@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
import { DocumentStatus, FieldType } from '@prisma/client';
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { prisma } from '@documenso/prisma';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedTeam } from '@documenso/prisma/seed/teams';
@ -34,7 +34,8 @@ test.describe('Signing Certificate Tests', () => {
},
})
.then(async (data) => {
const documentUrl = getEnvelopeDownloadUrl({
const documentUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: data,
token: recipient.token,
version: 'signed',
@ -85,7 +86,8 @@ test.describe('Signing Certificate Tests', () => {
const firstDocumentData = completedDocument.envelopeItems[0];
const documentUrl = getEnvelopeDownloadUrl({
const documentUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: firstDocumentData,
token: recipient.token,
version: 'signed',
@ -139,7 +141,8 @@ test.describe('Signing Certificate Tests', () => {
},
})
.then(async (data) => {
const documentUrl = getEnvelopeDownloadUrl({
const documentUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: data,
token: recipient.token,
version: 'signed',
@ -188,7 +191,8 @@ test.describe('Signing Certificate Tests', () => {
const firstDocumentData = completedDocument.envelopeItems[0];
const documentUrl = getEnvelopeDownloadUrl({
const documentUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: firstDocumentData,
token: recipient.token,
version: 'signed',
@ -242,7 +246,8 @@ test.describe('Signing Certificate Tests', () => {
},
})
.then(async (data) => {
const documentUrl = getEnvelopeDownloadUrl({
const documentUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: data,
token: recipient.token,
version: 'signed',
@ -289,7 +294,8 @@ test.describe('Signing Certificate Tests', () => {
},
});
const documentUrl = getEnvelopeDownloadUrl({
const documentUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: completedDocument.envelopeItems[0],
token: recipient.token,
version: 'signed',

View File

@ -1,6 +1,6 @@
import type { EnvelopeItem } from '@prisma/client';
import { getEnvelopeDownloadUrl } from '../utils/envelope-download';
import { getEnvelopeItemPdfUrl } from '../utils/envelope-download';
import { downloadFile } from './download-file';
type DocumentVersion = 'original' | 'signed';
@ -24,7 +24,8 @@ export const downloadPDF = async ({
fileName,
version = 'signed',
}: DownloadPDFProps) => {
const downloadUrl = getEnvelopeDownloadUrl({
const downloadUrl = getEnvelopeItemPdfUrl({
type: 'download',
envelopeItem: envelopeItem,
token,
version,

View File

@ -8,7 +8,7 @@ import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors';
import type { TEnvelope } from '../../types/envelope';
import type { FieldRenderMode } from '../../universal/field-renderer/render-field';
import { getEnvelopeDownloadUrl } from '../../utils/envelope-download';
import { getEnvelopeItemPdfUrl } from '../../utils/envelope-download';
type FileData =
| {
@ -124,10 +124,10 @@ export const EnvelopeRenderProvider = ({
}
try {
const downloadUrl = getEnvelopeDownloadUrl({
const downloadUrl = getEnvelopeItemPdfUrl({
type: 'view',
envelopeItem: envelopeItem,
token,
version: 'signed',
});
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());

View File

@ -32,6 +32,7 @@ export type JobDefinition<Name extends string = string, Schema = any> = {
name: string;
version: string;
enabled?: boolean;
optimizeParallelism?: boolean;
trigger: {
name: Name;
schema?: z.ZodType<Schema>;

View File

@ -40,6 +40,7 @@ export class InngestJobProvider extends BaseJobProvider {
{
id: job.id,
name: job.name,
optimizeParallelism: job.optimizeParallelism ?? false,
},
{
event: job.trigger.name,

View File

@ -189,29 +189,65 @@ export const run = async ({
settings,
});
const newDocumentData = await Promise.all(
envelopeItems.map(async (envelopeItem) =>
io.runTask(`decorate-and-sign-envelope-item-${envelopeItem.id}`, async () => {
const envelopeItemFields = envelope.envelopeItems.find(
(item) => item.id === envelopeItem.id,
)?.field;
// !: The commented out code is our desired implementation but we're seemingly
// !: running into issues with inngest parallelism in production.
// !: Until this is resolved we will do this sequentially which is slower but
// !: will actually work.
// const decoratePromises: Array<Promise<{ oldDocumentDataId: string; newDocumentDataId: string }>> =
// [];
if (!envelopeItemFields) {
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
}
// for (const envelopeItem of envelopeItems) {
// const task = io.runTask(`decorate-${envelopeItem.id}`, async () => {
// const envelopeItemFields = envelope.envelopeItems.find(
// (item) => item.id === envelopeItem.id,
// )?.field;
return decorateAndSignPdf({
envelope,
envelopeItem,
envelopeItemFields,
isRejected,
rejectionReason,
certificateData,
auditLogData,
});
}),
),
);
// if (!envelopeItemFields) {
// throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
// }
// return decorateAndSignPdf({
// envelope,
// envelopeItem,
// envelopeItemFields,
// isRejected,
// rejectionReason,
// certificateData,
// auditLogData,
// });
// });
// decoratePromises.push(task);
// }
// const newDocumentData = await Promise.all(decoratePromises);
// TODO: Remove once parallelization is working
const newDocumentData: Array<{ oldDocumentDataId: string; newDocumentDataId: string }> = [];
for (const envelopeItem of envelopeItems) {
const result = await io.runTask(`decorate-${envelopeItem.id}`, async () => {
const envelopeItemFields = envelope.envelopeItems.find(
(item) => item.id === envelopeItem.id,
)?.field;
if (!envelopeItemFields) {
throw new Error(`Envelope item fields not found for envelope item ${envelopeItem.id}`);
}
return decorateAndSignPdf({
envelope,
envelopeItem,
envelopeItemFields,
isRejected,
rejectionReason,
certificateData,
auditLogData,
});
});
newDocumentData.push(result);
}
const postHog = PostHogServerClient();

View File

@ -18,6 +18,7 @@ export const SEAL_DOCUMENT_JOB_DEFINITION = {
id: SEAL_DOCUMENT_JOB_DEFINITION_ID,
name: 'Seal Document',
version: '1.0.0',
optimizeParallelism: true,
trigger: {
name: SEAL_DOCUMENT_JOB_DEFINITION_ID,
schema: SEAL_DOCUMENT_JOB_DEFINITION_SCHEMA,

View File

@ -2,18 +2,33 @@ import type { EnvelopeItem } from '@prisma/client';
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
export type EnvelopeDownloadUrlOptions = {
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
version: 'original' | 'signed';
};
export type EnvelopeItemPdfUrlOptions =
| {
type: 'download';
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
version: 'original' | 'signed';
}
| {
type: 'view';
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
};
export const getEnvelopeDownloadUrl = (options: EnvelopeDownloadUrlOptions) => {
const { envelopeItem, token, version } = options;
export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
const { envelopeItem, token, type } = options;
const { id, envelopeId } = envelopeItem;
if (type === 'download') {
const version = options.version;
return token
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
}
return token
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}`
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}`;
};

View File

@ -12,7 +12,7 @@ import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { cn } from '../lib/utils';
import { useToast } from './use-toast';
@ -157,10 +157,10 @@ export const PDFViewer = ({
try {
setIsDocumentBytesLoading(true);
const documentUrl = getEnvelopeDownloadUrl({
const documentUrl = getEnvelopeItemPdfUrl({
type: 'view',
envelopeItem: envelopeItem,
token,
version,
});
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());