Files
triggerdotdev--trigger.dev/apps/webapp/app/utils.ts
T
Eric Allam 6ebd435e81 Feature: Run execution concurrency limits (#750)
* WIP execution concurrency controls implemented via Redis

- Split up resuming a run and executing a run
- Added some new statuses to better show what is going on in a run
- Removed preprocessing runs

* WIP

* Convert to using ZSETs and adding env vars

* Removed unused import

* Improve run number generation using advistory locks, and only on start

* More execution concurrency stuff

* Add support for job concurrency limits and concurrency limit groups

* Create wild-swans-battle.md

* Increase slots refresh timeout to 10s

* Try to fix Redis connection issues

* Don’t be so strict about the APP_ENV

* Add the blank tls option to the normal redis client as well

* Add docs
2023-11-28 16:21:06 +00:00

168 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { UIMatch } from "@remix-run/react";
import { useMatches } from "@remix-run/react";
import humanizeDuration from "humanize-duration";
const DEFAULT_REDIRECT = "/";
/**
* This should be used any time the redirect path is user-provided
* (Like the query string on our login/signup pages). This avoids
* open-redirect vulnerabilities.
* @param {string} to The redirect destination
* @param {string} defaultRedirect The redirect to use if the to is unsafe.
*/
export function safeRedirect(
to: FormDataEntryValue | string | null | undefined,
defaultRedirect: string = DEFAULT_REDIRECT
) {
if (!to || typeof to !== "string") {
return defaultRedirect;
}
if (!to.startsWith("/") || to.startsWith("//")) {
return defaultRedirect;
}
return to;
}
/**
* This base hook is used in other hooks to quickly search for specific data
* across all loader data using useMatches.
* @param {string} id The route id
* @returns {JSON|undefined} The router data or undefined if not found
*/
export function useMatchesData(id: string | string[], debug: boolean = false): UIMatch | undefined {
const matchingRoutes = useMatches();
if (debug) {
console.log("matchingRoutes", matchingRoutes);
}
const paths = Array.isArray(id) ? id : [id];
// Get the first matching route
const route = paths.reduce((acc, path) => {
if (acc) return acc;
return matchingRoutes.find((route) => route.id === path);
}, undefined as UIMatch | undefined);
return route;
}
export function validateEmail(email: unknown): email is string {
return typeof email === "string" && email.length > 3 && email.includes("@");
}
export function hydrateObject<T>(object: any): T {
return hydrateDates(object) as T;
}
export function hydrateDates(object: any): any {
if (object === null || object === undefined) {
return object;
}
if (object instanceof Date) {
return object;
}
if (
typeof object === "string" &&
object.match(/\d{4}-\d{2}-\d{2}/) &&
!Number.isNaN(Date.parse(object))
) {
return new Date(object);
}
if (typeof object === "object") {
if (Array.isArray(object)) {
return object.map((item) => hydrateDates(item));
} else {
const hydratedObject: any = {};
for (const key in object) {
hydratedObject[key] = hydrateDates(object[key]);
}
return hydratedObject;
}
}
return object;
}
type DurationOptions = {
style?: "long" | "short";
maxDecimalPoints?: number;
};
export function formatDuration(
start?: Date | null,
end?: Date | null,
options?: DurationOptions
): string {
if (!start || !end) {
return "";
}
return formatDurationMilliseconds(dateDifference(start, end), options);
}
export function formatDurationMilliseconds(
milliseconds: number,
options?: DurationOptions
): string {
let duration = humanizeDuration(milliseconds, {
maxDecimalPoints: options?.maxDecimalPoints ?? 1,
largest: 2,
});
if (!options) {
return duration;
}
switch (options.style) {
case "short":
duration = duration.replace(" seconds", "s");
duration = duration.replace(" second", "s");
duration = duration.replace(" minutes", "m");
duration = duration.replace(" minute", "m");
duration = duration.replace(" hours", "h");
duration = duration.replace(" hour", "h");
duration = duration.replace(" days", "d");
duration = duration.replace(" day", "d");
duration = duration.replace(" weeks", "w");
duration = duration.replace(" week", "w");
duration = duration.replace(" months", "mo");
duration = duration.replace(" month", "mo");
duration = duration.replace(" years", "y");
duration = duration.replace(" year", "y");
}
return duration;
}
export function titleCase(original: string): string {
return original
.split(" ")
.map((word) => word[0].toUpperCase() + word.slice(1))
.join(" ");
}
function dateDifference(date1: Date, date2: Date) {
return Math.abs(date1.getTime() - date2.getTime());
}
// Takes an api key (either trigger_live_xxxx or trigger_development_xxxx) and returns trigger_live_********
export const obfuscateApiKey = (apiKey: string) => {
const [prefix, slug, secretPart] = apiKey.split("_");
return `${prefix}_${slug}_${"*".repeat(secretPart.length)}`;
};
export function appEnvTitleTag(appEnv?: string): string {
if (!appEnv || appEnv === "production") {
return "";
}
return ` (${appEnv})`
}