fix: js command queue support for late initialization (#2974)

This commit is contained in:
Anshuman Pandey
2024-08-21 16:42:30 +05:30
committed by GitHub
parent d84146fd88
commit c108cd4780
4 changed files with 51 additions and 111 deletions

View File

@@ -128,6 +128,7 @@ const AppPage = ({}) => {
}}>
Reset
</button>
<p className="text-xs text-slate-700 dark:text-slate-300">
If you made a change in Formbricks app and it does not seem to work, hit &apos;Reset&apos; and
try again.

View File

@@ -1,38 +0,0 @@
// Simple queue for formbricks methods
export class MethodQueue {
private queue: (() => Promise<unknown>)[] = [];
private isExecuting = false;
add = (method: () => Promise<unknown>): void => {
this.queue.push(method);
void this.run();
};
private runNext = async (): Promise<void> => {
if (this.isExecuting) return;
const method = this.queue.shift();
if (method) {
this.isExecuting = true;
try {
await method();
} finally {
this.isExecuting = false;
if (this.queue.length > 0) {
void this.runNext();
}
}
}
};
run = async (): Promise<void> => {
if (!this.isExecuting && this.queue.length > 0) {
await this.runNext();
}
};
clear = (): void => {
this.queue = [];
};
}

View File

@@ -1,26 +1,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access --
* Required for dynamic function calls
*/
/* eslint-disable @typescript-eslint/no-unsafe-call --
* Required for dynamic function calls
*/
/*
eslint-disable no-console --
eslint-disable no-console --
* Required for logging errors
*/
import { type Result, wrapThrowsAsync } from "@formbricks/types/error-handlers";
import { MethodQueue } from "../method-queue";
import { type Result } from "@formbricks/types/error-handlers";
let isInitializing = false;
let isInitialized = false;
const methodQueue = new MethodQueue();
// Load the SDK, return the result
const loadFormbricksSDK = async (apiHost: string, sdkType: "app" | "website"): Promise<Result<void>> => {
const loadFormbricksSDK = async (apiHostParam: string, sdkType: "app" | "website"): Promise<Result<void>> => {
if (!window.formbricks) {
const res = await fetch(`${apiHost}/api/packages/${sdkType}`);
const res = await fetch(`${apiHostParam}/api/packages/${sdkType}`);
// Failed to fetch the app package
if (!res.ok) {
@@ -63,81 +53,62 @@ const loadFormbricksSDK = async (apiHost: string, sdkType: "app" | "website"): P
return { ok: true, data: undefined };
};
// TODO: @pandeymangg - Fix these types
// type FormbricksAppMethods = {
// [K in keyof TFormbricksApp]: TFormbricksApp[K] extends Function ? K : never;
// }[keyof TFormbricksApp];
// type FormbricksWebsiteMethods = {
// [K in keyof TFormbricksWebsite]: TFormbricksWebsite[K] extends Function ? K : never;
// }[keyof TFormbricksWebsite];
const functionsToProcess: { prop: string; args: unknown[] }[] = [];
export const loadFormbricksToProxy = async (
prop: string,
sdkType: "app" | "website",
...args: unknown[]
// eslint-disable-next-line @typescript-eslint/require-await -- Required for dynamic function calls
): Promise<void> => {
const executeMethod = async (): Promise<unknown> => {
try {
if (window.formbricks) {
// @ts-expect-error -- window.formbricks is a dynamic function
return (await window.formbricks[prop](...args)) as unknown;
}
} catch (error: unknown) {
console.error("🧱 Formbricks - Global error: ", error);
throw error;
}
};
// all of this should happen when not initialized:
if (!isInitialized) {
if (isInitializing) {
methodQueue.add(executeMethod);
} else if (prop === "init") {
if (prop === "init") {
if (isInitializing) {
console.warn("🧱 Formbricks - Warning: Formbricks is already initializing.");
return;
}
// reset the initialization state
isInitializing = true;
const initialize = async (): Promise<unknown> => {
const { apiHost } = args[0] as { apiHost: string };
const loadSDKResult = (await wrapThrowsAsync(loadFormbricksSDK)(apiHost, sdkType)) as unknown as {
ok: boolean;
error: Error;
};
const apiHost = (args[0] as { apiHost: string }).apiHost;
const loadSDKResult = await loadFormbricksSDK(apiHost, sdkType);
if (loadSDKResult.ok) {
if (window.formbricks) {
// @ts-expect-error -- Required for dynamic function calls
void window.formbricks.init(...args);
if (!loadSDKResult.ok) {
isInitializing = false;
console.error(`🧱 Formbricks - Global error: ${loadSDKResult.error.message}`);
return;
}
isInitialized = true;
try {
if (window.formbricks) {
// @ts-expect-error -- args is an array
await window.formbricks[prop](...args);
isInitialized = true;
isInitializing = false;
// process the queued functions
for (const { prop: functionProp, args: functionArgs } of functionsToProcess) {
type FormbricksProp = keyof typeof window.formbricks;
if (typeof window.formbricks[functionProp as FormbricksProp] !== "function") {
console.error(`🧱 Formbricks - Error: Method ${functionProp} does not exist on formbricks`);
continue;
}
// @ts-expect-error -- Required for dynamic function calls
(window.formbricks[functionProp] as unknown)(...functionArgs);
}
} catch (error) {
isInitializing = false;
console.error("🧱 Formbricks - Global error: ", error);
throw error;
}
};
methodQueue.add(initialize);
}
} else {
console.error(
"🧱 Formbricks - Global error: You need to call formbricks.init before calling any other method"
console.warn(
"🧱 Formbricks - Warning: Formbricks not initialized. This method will be queued and executed after initialization."
);
}
} else {
// @ts-expect-error -- window.formbricks is a dynamic function
if (window.formbricks && typeof window.formbricks[prop] !== "function") {
console.error(
`🧱 Formbricks - Global error: Formbricks ${sdkType} SDK does not support method ${String(prop)}`
);
return;
}
methodQueue.add(executeMethod);
functionsToProcess.push({ prop, args });
}
} else if (window.formbricks) {
type Formbricks = typeof window.formbricks;
type FunctionProp = keyof Formbricks;
const functionPropTyped = prop as FunctionProp;
// @ts-expect-error -- Required for dynamic function calls
await window.formbricks[functionPropTyped](...args);
}
};

View File

@@ -47,6 +47,12 @@
"persistent": true,
"dependsOn": ["@formbricks/api#build"]
},
"@formbricks/js#lint": {
"dependsOn": ["@formbricks/js-core#build"]
},
"@formbricks/database#lint": {
"dependsOn": ["@formbricks/database#build"]
},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],