From 03df00fa72f858278ef551b5625ee245e2e69976 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 24 Jun 2026 13:54:40 +0200 Subject: [PATCH] :constructor: Add more fixes --- performance/lib/penpot-client.js | 42 ++++++++++++ .../scripts/workspace-edit-concurrent.js | 64 ++++++++++++++++--- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/performance/lib/penpot-client.js b/performance/lib/penpot-client.js index f59ee3134e..0f8dfe26c0 100644 --- a/performance/lib/penpot-client.js +++ b/performance/lib/penpot-client.js @@ -535,6 +535,46 @@ export function createClient(baseUrl) { }; } + /** + * Invite members to a team by email. + * + * @param {string} teamId - Team UUID + * @param {string[]} emails - Array of email addresses + * @param {string} role - Role for the invited members (e.g. "editor") + * @returns {object} Parsed response { status, body } + */ + function inviteTeamMembers(teamId, emails, role) { + const res = rpc("POST", "create-team-invitations", { + "team-id": teamId, + emails: emails, + role: role, + }); + return { + status: res.status, + body: res.status === 200 ? res.json() : null, + raw: res, + }; + } + + /** + * Get an invitation token for a specific email. + * + * @param {string} teamId - Team UUID + * @param {string} email - Invited email address + * @returns {object} Parsed response { status, body } + */ + function getTeamInvitationToken(teamId, email) { + const res = rpc("GET", "get-team-invitation-token", { + "team-id": teamId, + email: email, + }); + return { + status: res.status, + body: res.status === 200 ? res.json() : null, + raw: res, + }; + } + /** * Logout the current user. * @@ -574,6 +614,8 @@ export function createClient(baseUrl) { deleteFile, deleteProject, deleteTeam, + inviteTeamMembers, + getTeamInvitationToken, logout, }; } diff --git a/performance/scripts/workspace-edit-concurrent.js b/performance/scripts/workspace-edit-concurrent.js index 08adc82314..3164982b1e 100644 --- a/performance/scripts/workspace-edit-concurrent.js +++ b/performance/scripts/workspace-edit-concurrent.js @@ -151,11 +151,39 @@ export function setup() { } console.log(` Created ${users.length} demo profiles`); - // Login with first user to create project and files + // Login with first user to create shared team and files const loginRes = client.login(users[0].email, users[0].password); if (loginRes.status !== 200) fail("Login failed for setup"); - const teamId = client.getTeams().body[0].id; - const projectId = client.createProject(teamId, "Concurrent Edit Project").body.id; + + // Create a shared team so all VUs can access the same file. + // Each demo profile gets its own default team; without a shared + // team, VUs 2+ would get 404 on get-file. + const teamRes = client.createTeam("Concurrent Edit Team"); + if (teamRes.status !== 200) fail("Failed to create shared team"); + const sharedTeamId = teamRes.body.id; + console.log(` Shared team: ${sharedTeamId}`); + + // Invite remaining users to the shared team and get acceptance tokens. + // The tokens are used by each VU via verify-token to join the team. + const invitationTokens = []; + for (let i = 1; i < TOTAL_VUS; i++) { + const invRes = client.inviteTeamMembers(sharedTeamId, [users[i].email], "editor"); + if (invRes.status !== 200) { + console.error(` Invite user ${i}: status=${invRes.status}`); + } + const tokenRes = client.getTeamInvitationToken(sharedTeamId, users[i].email); + if (tokenRes.status === 200 && tokenRes.body) { + invitationTokens.push({ vuIndex: i, token: tokenRes.body }); + } + } + if (invitationTokens.length > 0) { + console.log(` Got ${invitationTokens.length} invitation tokens`); + } else { + console.log(` All users auto-added (no tokens needed)`); + } + + // Create project and files in the shared team + const projectId = client.createProject(sharedTeamId, "Concurrent Edit Project").body.id; console.log(` Project: ${projectId}`); // Build file/page assignments based on mode @@ -180,13 +208,14 @@ export function setup() { const pageIds = [defaultPageId]; // Add remaining pages + // vern never changes on regular edits (only on snapshot restore), + // and each add-page increments revn by 1, so no need to re-fetch. for (let i = 1; i < TOTAL_VUS; i++) { const pageId = uuidv4(); const pageName = `Page ${i + 1}`; const addRes = addPage(client, fileId, revn, vern, pageId, pageName); if (addRes.status !== 200) fail(`Failed to add page ${i + 1}`); - revn = addRes.body.revn; - vern = addRes.body.vern; + revn++; pageIds.push(pageId); } console.log(` Added ${pageIds.length} pages to file`); @@ -222,8 +251,7 @@ export function setup() { const pageName = `Page ${p + 1}`; const addRes = addPage(client, fileId, revn, vern, pageId, pageName); if (addRes.status !== 200) fail(`Failed to add page ${p + 1} to file ${f + 1}`); - revn = addRes.body.revn; - vern = addRes.body.vern; + revn++; pageIds.push(pageId); } console.log(` Added ${pageIds.length} pages to file ${f + 1}`); @@ -246,6 +274,7 @@ export function setup() { editMode: EDIT_MODE, users, vuAssignments, + invitationTokens, }; } @@ -253,10 +282,17 @@ export function setup() { // Main VU Function — each VU edits its assigned page // --------------------------------------------------------------------------- +// Track which VUs have accepted their invitation (once per VU, not per iteration) +const verifiedVus = {}; + +// --------------------------------------------------------------------------- +// Main VU Function — each VU edits its assigned page +// --------------------------------------------------------------------------- + export default function (data) { const client = createClient(data.baseUrl); - // Pick user and assignment from pool + // Each VU uses its own demo profile (different users editing the same file) const vuIndex = __VU - 1; const user = data.users[vuIndex]; const assignment = data.vuAssignments[vuIndex]; @@ -268,6 +304,18 @@ export default function (data) { // Login if (!assertOk(client.login(user.email, user.password), "login")) fail("login failed"); + + // Accept team invitation once per VU (not per iteration). + // In devenv the user may already be auto-added; 400 on already-accepted + // tokens is harmless — skip the token on subsequent iterations. + if (!verifiedVus[__VU]) { + const tokenEntry = data.invitationTokens.find((t) => t.vuIndex === vuIndex); + if (tokenEntry && tokenEntry.token) { + client.rpc("POST", "verify-token", tokenEntry.token); + } + verifiedVus[__VU] = true; + } + sleep(0.5); // Edit loop