Jinho Kim
Jinho Kim12mo ago

How can I use timeout in mutation?

I have set up the following configuration using Clerk webhooks: 1. When an organization is created, a Document of type "workspace" is also created with the same data. 2. When an organization membership is created, a Document of type "workspace membership" is generated. The issue arises when creating a workspace, as both steps 1 and 2 occur simultaneously. During the process of creating a workspace through step 1, if step 2 is triggered before the Doc<"workspace"> is created, it results in an error because the Doc<"workspace_membership"> cannot be created without the workspace existing. To address this, I implemented code that uses a timeout to wait until the workspace is created before proceeding with creating the workspace membership. However, using a timeout within the mutation has led to errors. Is there a better way to handle this situation?
No description
6 Replies
Jinho Kim
Jinho KimOP12mo ago
This is the code.
export const _update_or_create_by_clerk_membership = internalMutation({
args: { clerk_membership: v.any() },
async handler(
ctx,
{
clerk_membership,
}: {
clerk_membership: OrganizationMembershipJSON;
}
) {
const existing_membership = await query_by_clerk_org_id(ctx, clerk_membership.id);

let entity_id: Id<'users'> | null = null;
let workspace_id: Id<'workspaces'> | null = null;

const pollForWorkspaceCreation = async (clerkOrgId: string) => {
const maxAttempts = 10;
let attempts = 0;
while (attempts < maxAttempts) {
const workspace = await query_workspace_by_clerk_id(ctx, clerkOrgId);
if (workspace) {
return workspace;
}
await new Promise((resolve) => setTimeout(resolve, 2000));
attempts++;
}
throw new Error('Workspace creation timed out');
};

if (existing_membership) {
entity_id = existing_membership.entity_id as Id<'users'>;
workspace_id = existing_membership.workspace_id;
} else {
const user = await query_user_by_clerk_id(ctx, clerk_membership.public_user_data.user_id);
if (!user) {
throw new Error('Unauthorized');
}

const workspace = await pollForWorkspaceCreation(clerk_membership.organization.id);
if (workspace === null) {
throw new Error('Workspace not found after retries');
}
entity_id = user._id;
workspace_id = workspace._id;
}

const membership_data: {...}

if (existing_membership === null) {
await ctx.db.insert('workspace_memberships', membership_data);
} else {
await ctx.db.patch(existing_membership._id, membership_data);
}
},
});
export const _update_or_create_by_clerk_membership = internalMutation({
args: { clerk_membership: v.any() },
async handler(
ctx,
{
clerk_membership,
}: {
clerk_membership: OrganizationMembershipJSON;
}
) {
const existing_membership = await query_by_clerk_org_id(ctx, clerk_membership.id);

let entity_id: Id<'users'> | null = null;
let workspace_id: Id<'workspaces'> | null = null;

const pollForWorkspaceCreation = async (clerkOrgId: string) => {
const maxAttempts = 10;
let attempts = 0;
while (attempts < maxAttempts) {
const workspace = await query_workspace_by_clerk_id(ctx, clerkOrgId);
if (workspace) {
return workspace;
}
await new Promise((resolve) => setTimeout(resolve, 2000));
attempts++;
}
throw new Error('Workspace creation timed out');
};

if (existing_membership) {
entity_id = existing_membership.entity_id as Id<'users'>;
workspace_id = existing_membership.workspace_id;
} else {
const user = await query_user_by_clerk_id(ctx, clerk_membership.public_user_data.user_id);
if (!user) {
throw new Error('Unauthorized');
}

const workspace = await pollForWorkspaceCreation(clerk_membership.organization.id);
if (workspace === null) {
throw new Error('Workspace not found after retries');
}
entity_id = user._id;
workspace_id = workspace._id;
}

const membership_data: {...}

if (existing_membership === null) {
await ctx.db.insert('workspace_memberships', membership_data);
} else {
await ctx.db.patch(existing_membership._id, membership_data);
}
},
});
erquhart
erquhart12mo ago
I would avoid the timeout approach as it has no guarantees. There are definitely options, though, but I need to understand your context a little more. I'm guessing you're using Clerk's org components and getting a webhook when an org is created. When is the function above called? If a user creates a new organization through your Clerk component, and the user is made a member of the Clerk org as a part of that process, I would have the webhook get the users in the new org from Clerk and create the records necessary in your Convex tables. Then you just need to have your UI show a loading state until the records are created via webhook. Again, that's just a stab without knowing the details of your implementation.
lee
lee12mo ago
(another option close to the timeout approach is to have the mutation schedule itself with a little delay)
if (org doesn't exist yet) {
await ctx.scheduler.runAsync(100, api.update_or_create_etc, args);
return;
}
if (org doesn't exist yet) {
await ctx.scheduler.runAsync(100, api.update_or_create_etc, args);
return;
}
erquhart
erquhart12mo ago
Just realized setTimeout not being available in mutations is your core issue, this is what I get for trying to help on mobile lol. Lee's answer is likely what you're looking for.
lee
lee12mo ago
btw even if setTimeout did work in mutations, you probably wouldn't get what you expect because mutations are transactions. So if you read a document twice, you'll always get the same result no matter how long the mutation takes.
Michal Srb
Michal Srb12mo ago
I agree with @erquhart that this should not require waiting. Your issue is you're getting a request A and request B, in arbitrary order. But if B comes before A, it cannot proceed. So if B came first, I would store all the required information from B into the database, and when A comes in I would check whether the info from B was stored, and if so perform A and then whatever B would have done.

Did you find this page helpful?