fix v2 release CI failures
This commit is contained in:
1 parent
4ac9d21bc1
commit
72caa31b57
5 files changed
+278
-218
No files matched your search
@@ -12,6 +12,7 @@ COPY apps/docs ./apps/docs
|
|||||||
COPY packages/custom-widgets ./packages/custom-widgets
|
COPY packages/custom-widgets ./packages/custom-widgets
|
||||||
COPY packages/definitions ./packages/definitions
|
COPY packages/definitions ./packages/definitions
|
||||||
COPY packages/workshop ./packages/workshop
|
COPY packages/workshop ./packages/workshop
|
||||||
|
COPY scripts/build-workshop-validator.mjs ./scripts/build-workshop-validator.mjs
|
||||||
COPY tooling/typescript ./tooling/typescript
|
COPY tooling/typescript ./tooling/typescript
|
||||||
RUN pnpm config set store-dir /pnpm/store && pnpm install --filter @homarr/docs... --frozen-lockfile
|
RUN pnpm config set store-dir /pnpm/store && pnpm install --filter @homarr/docs... --frozen-lockfile
|
||||||
RUN pnpm build:workshop-validator
|
RUN pnpm build:workshop-validator
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import { encryptSecret } from "@homarr/common/server";
|
||||||
|
import type { Database } from "@homarr/db";
|
||||||
|
import { and, eq, handleTransactionsAsync } from "@homarr/db";
|
||||||
|
import { customWidgetDefinitions, customWidgetSecrets } from "@homarr/db/schema";
|
||||||
|
import type {
|
||||||
|
CustomWidgetCreateInput,
|
||||||
|
CustomWidgetSecretKind,
|
||||||
|
CustomWidgetSource,
|
||||||
|
HomarrCustomWidgetV2,
|
||||||
|
} from "@homarr/custom-widgets/core";
|
||||||
|
|
||||||
|
import { hasSameSecretBinding } from "./secret-policy";
|
||||||
|
import { serializeCustomWidgetDefinition } from "./stored-definition";
|
||||||
|
|
||||||
|
type CustomWidgetSecretInput = CustomWidgetCreateInput["secrets"][number];
|
||||||
|
|
||||||
|
export async function setCustomWidgetSecret(db: Database, definitionId: string, secret: CustomWidgetSecretInput) {
|
||||||
|
const updatedAt = new Date();
|
||||||
|
const secretRow = {
|
||||||
|
definitionId,
|
||||||
|
sourceId: secret.sourceId,
|
||||||
|
kind: secret.kind,
|
||||||
|
encryptedValue: encryptSecret(secret.value),
|
||||||
|
updatedAt,
|
||||||
|
};
|
||||||
|
await handleTransactionsAsync(db, {
|
||||||
|
async handleAsync(database, schema) {
|
||||||
|
await database.transaction(async (transaction) => {
|
||||||
|
await transaction
|
||||||
|
.delete(schema.customWidgetSecrets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.customWidgetSecrets.definitionId, definitionId),
|
||||||
|
eq(schema.customWidgetSecrets.sourceId, secret.sourceId),
|
||||||
|
eq(schema.customWidgetSecrets.kind, secret.kind),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await transaction.insert(schema.customWidgetSecrets).values(secretRow);
|
||||||
|
await transaction
|
||||||
|
.update(schema.customWidgetDefinitions)
|
||||||
|
.set({ updatedAt })
|
||||||
|
.where(eq(schema.customWidgetDefinitions.id, definitionId));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSync(database) {
|
||||||
|
database.transaction((transaction) => {
|
||||||
|
transaction
|
||||||
|
.delete(customWidgetSecrets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(customWidgetSecrets.definitionId, definitionId),
|
||||||
|
eq(customWidgetSecrets.sourceId, secret.sourceId),
|
||||||
|
eq(customWidgetSecrets.kind, secret.kind),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
transaction.insert(customWidgetSecrets).values(secretRow).run();
|
||||||
|
transaction
|
||||||
|
.update(customWidgetDefinitions)
|
||||||
|
.set({ updatedAt })
|
||||||
|
.where(eq(customWidgetDefinitions.id, definitionId))
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function configureCustomWidgetSource(
|
||||||
|
db: Database,
|
||||||
|
input: {
|
||||||
|
definitionId: string;
|
||||||
|
sourceId: string;
|
||||||
|
definition: HomarrCustomWidgetV2;
|
||||||
|
previousSource: CustomWidgetSource;
|
||||||
|
source: CustomWidgetSource;
|
||||||
|
secrets: CustomWidgetCreateInput["secrets"];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const definitionChanges = {
|
||||||
|
...serializeCustomWidgetDefinition(input.definition),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
const bindingChanged = !hasSameSecretBinding(input.previousSource, input.source);
|
||||||
|
const secretRows = input.secrets.map((secret) => ({
|
||||||
|
definitionId: input.definitionId,
|
||||||
|
sourceId: input.sourceId,
|
||||||
|
kind: secret.kind,
|
||||||
|
encryptedValue: encryptSecret(secret.value),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}));
|
||||||
|
await handleTransactionsAsync(db, {
|
||||||
|
async handleAsync(database, schema) {
|
||||||
|
await database.transaction(async (transaction) => {
|
||||||
|
await transaction
|
||||||
|
.update(schema.customWidgetDefinitions)
|
||||||
|
.set(definitionChanges)
|
||||||
|
.where(eq(schema.customWidgetDefinitions.id, input.definitionId));
|
||||||
|
if (bindingChanged) {
|
||||||
|
await transaction
|
||||||
|
.delete(schema.customWidgetSecrets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.customWidgetSecrets.definitionId, input.definitionId),
|
||||||
|
eq(schema.customWidgetSecrets.sourceId, input.sourceId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const secret of secretRows) {
|
||||||
|
await transaction
|
||||||
|
.delete(schema.customWidgetSecrets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.customWidgetSecrets.definitionId, input.definitionId),
|
||||||
|
eq(schema.customWidgetSecrets.sourceId, input.sourceId),
|
||||||
|
eq(schema.customWidgetSecrets.kind, secret.kind),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await transaction.insert(schema.customWidgetSecrets).values(secret);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSync(database) {
|
||||||
|
database.transaction((transaction) => {
|
||||||
|
transaction
|
||||||
|
.update(customWidgetDefinitions)
|
||||||
|
.set(definitionChanges)
|
||||||
|
.where(eq(customWidgetDefinitions.id, input.definitionId))
|
||||||
|
.run();
|
||||||
|
if (bindingChanged) {
|
||||||
|
transaction
|
||||||
|
.delete(customWidgetSecrets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(customWidgetSecrets.definitionId, input.definitionId),
|
||||||
|
eq(customWidgetSecrets.sourceId, input.sourceId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
for (const secret of secretRows) {
|
||||||
|
transaction
|
||||||
|
.delete(customWidgetSecrets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(customWidgetSecrets.definitionId, input.definitionId),
|
||||||
|
eq(customWidgetSecrets.sourceId, input.sourceId),
|
||||||
|
eq(customWidgetSecrets.kind, secret.kind),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
transaction.insert(customWidgetSecrets).values(secret).run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clearCustomWidgetSecret(
|
||||||
|
db: Database,
|
||||||
|
input: {
|
||||||
|
definitionId: string;
|
||||||
|
sourceId: string;
|
||||||
|
kind: CustomWidgetSecretKind;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const updatedAt = new Date();
|
||||||
|
await handleTransactionsAsync(db, {
|
||||||
|
async handleAsync(database, schema) {
|
||||||
|
await database.transaction(async (transaction) => {
|
||||||
|
await transaction
|
||||||
|
.delete(schema.customWidgetSecrets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(schema.customWidgetSecrets.definitionId, input.definitionId),
|
||||||
|
eq(schema.customWidgetSecrets.sourceId, input.sourceId),
|
||||||
|
eq(schema.customWidgetSecrets.kind, input.kind),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await transaction
|
||||||
|
.update(schema.customWidgetDefinitions)
|
||||||
|
.set({ updatedAt })
|
||||||
|
.where(eq(schema.customWidgetDefinitions.id, input.definitionId));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSync(database) {
|
||||||
|
database.transaction((transaction) => {
|
||||||
|
transaction
|
||||||
|
.delete(customWidgetSecrets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(customWidgetSecrets.definitionId, input.definitionId),
|
||||||
|
eq(customWidgetSecrets.sourceId, input.sourceId),
|
||||||
|
eq(customWidgetSecrets.kind, input.kind),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
transaction
|
||||||
|
.update(customWidgetDefinitions)
|
||||||
|
.set({ updatedAt })
|
||||||
|
.where(eq(customWidgetDefinitions.id, input.definitionId))
|
||||||
|
.run();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
import { encryptSecret } from "@homarr/common/server";
|
import { eq } from "@homarr/db";
|
||||||
import { and, eq, handleTransactionsAsync } from "@homarr/db";
|
import { customWidgetDefinitions } from "@homarr/db/schema";
|
||||||
import { customWidgetDefinitions, customWidgetSecrets } from "@homarr/db/schema";
|
|
||||||
import {
|
import {
|
||||||
customWidgetSecretInputSchema,
|
customWidgetSecretInputSchema,
|
||||||
customWidgetSecretsInputSchema,
|
customWidgetSecretsInputSchema,
|
||||||
@@ -16,9 +15,10 @@ import {
|
|||||||
createCustomWidgetConfigurationRequest,
|
createCustomWidgetConfigurationRequest,
|
||||||
getCustomWidgetConfigurationRequestForUser,
|
getCustomWidgetConfigurationRequestForUser,
|
||||||
} from "./configuration-requests";
|
} from "./configuration-requests";
|
||||||
import { assertSecretSources, hasSameSecretBinding, requiredSecretKinds } from "./secret-policy";
|
import { assertSecretSources, requiredSecretKinds } from "./secret-policy";
|
||||||
|
import { clearCustomWidgetSecret, configureCustomWidgetSource, setCustomWidgetSecret } from "./secret-persistence";
|
||||||
import { getPreviewSession } from "./preview-sessions";
|
import { getPreviewSession } from "./preview-sessions";
|
||||||
import { parseStoredCustomWidgetDefinition, serializeCustomWidgetDefinition } from "./stored-definition";
|
import { parseStoredCustomWidgetDefinition } from "./stored-definition";
|
||||||
|
|
||||||
const secretRequestInputSchema = z
|
const secretRequestInputSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -51,54 +51,7 @@ export const secretProcedures = {
|
|||||||
if (!existing) throw new TRPCError({ code: "NOT_FOUND" });
|
if (!existing) throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
const definition = parseStoredCustomWidgetDefinition(existing);
|
const definition = parseStoredCustomWidgetDefinition(existing);
|
||||||
assertSecretSources(definition.sources, [input.secret]);
|
assertSecretSources(definition.sources, [input.secret]);
|
||||||
const updatedAt = new Date();
|
await setCustomWidgetSecret(ctx.db, input.definitionId, input.secret);
|
||||||
const secretRow = {
|
|
||||||
definitionId: input.definitionId,
|
|
||||||
sourceId: input.secret.sourceId,
|
|
||||||
kind: input.secret.kind,
|
|
||||||
encryptedValue: encryptSecret(input.secret.value),
|
|
||||||
updatedAt,
|
|
||||||
};
|
|
||||||
await handleTransactionsAsync(ctx.db, {
|
|
||||||
async handleAsync(db, schema) {
|
|
||||||
await db.transaction(async (transaction) => {
|
|
||||||
await transaction
|
|
||||||
.delete(schema.customWidgetSecrets)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(schema.customWidgetSecrets.definitionId, input.definitionId),
|
|
||||||
eq(schema.customWidgetSecrets.sourceId, input.secret.sourceId),
|
|
||||||
eq(schema.customWidgetSecrets.kind, input.secret.kind),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await transaction.insert(schema.customWidgetSecrets).values(secretRow);
|
|
||||||
await transaction
|
|
||||||
.update(schema.customWidgetDefinitions)
|
|
||||||
.set({ updatedAt })
|
|
||||||
.where(eq(schema.customWidgetDefinitions.id, input.definitionId));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleSync(db) {
|
|
||||||
db.transaction((transaction) => {
|
|
||||||
transaction
|
|
||||||
.delete(customWidgetSecrets)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(customWidgetSecrets.definitionId, input.definitionId),
|
|
||||||
eq(customWidgetSecrets.sourceId, input.secret.sourceId),
|
|
||||||
eq(customWidgetSecrets.kind, input.secret.kind),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
transaction.insert(customWidgetSecrets).values(secretRow).run();
|
|
||||||
transaction
|
|
||||||
.update(customWidgetDefinitions)
|
|
||||||
.set({ updatedAt })
|
|
||||||
.where(eq(customWidgetDefinitions.id, input.definitionId))
|
|
||||||
.run();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return { sourceId: input.secret.sourceId, kind: input.secret.kind, isSet: true };
|
return { sourceId: input.secret.sourceId, kind: input.secret.kind, isSet: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -128,79 +81,13 @@ export const secretProcedures = {
|
|||||||
});
|
});
|
||||||
assertSecretSources({ [input.sourceId]: source }, input.secrets);
|
assertSecretSources({ [input.sourceId]: source }, input.secrets);
|
||||||
const updated = { ...definition, sources: { ...definition.sources, [input.sourceId]: source } };
|
const updated = { ...definition, sources: { ...definition.sources, [input.sourceId]: source } };
|
||||||
const definitionChanges = { ...serializeCustomWidgetDefinition(updated), updatedAt: new Date() };
|
await configureCustomWidgetSource(ctx.db, {
|
||||||
const bindingChanged = !hasSameSecretBinding(current, source);
|
|
||||||
const secretRows = input.secrets.map((secret) => ({
|
|
||||||
definitionId: input.definitionId,
|
definitionId: input.definitionId,
|
||||||
sourceId: input.sourceId,
|
sourceId: input.sourceId,
|
||||||
kind: secret.kind,
|
definition: updated,
|
||||||
encryptedValue: encryptSecret(secret.value),
|
previousSource: current,
|
||||||
updatedAt: new Date(),
|
source,
|
||||||
}));
|
secrets: input.secrets,
|
||||||
await handleTransactionsAsync(ctx.db, {
|
|
||||||
async handleAsync(db, schema) {
|
|
||||||
await db.transaction(async (transaction) => {
|
|
||||||
await transaction
|
|
||||||
.update(schema.customWidgetDefinitions)
|
|
||||||
.set(definitionChanges)
|
|
||||||
.where(eq(schema.customWidgetDefinitions.id, input.definitionId));
|
|
||||||
if (bindingChanged) {
|
|
||||||
await transaction
|
|
||||||
.delete(schema.customWidgetSecrets)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(schema.customWidgetSecrets.definitionId, input.definitionId),
|
|
||||||
eq(schema.customWidgetSecrets.sourceId, input.sourceId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const secret of secretRows) {
|
|
||||||
await transaction
|
|
||||||
.delete(schema.customWidgetSecrets)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(schema.customWidgetSecrets.definitionId, input.definitionId),
|
|
||||||
eq(schema.customWidgetSecrets.sourceId, input.sourceId),
|
|
||||||
eq(schema.customWidgetSecrets.kind, secret.kind),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await transaction.insert(schema.customWidgetSecrets).values(secret);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleSync(db) {
|
|
||||||
db.transaction((transaction) => {
|
|
||||||
transaction
|
|
||||||
.update(customWidgetDefinitions)
|
|
||||||
.set(definitionChanges)
|
|
||||||
.where(eq(customWidgetDefinitions.id, input.definitionId))
|
|
||||||
.run();
|
|
||||||
if (bindingChanged) {
|
|
||||||
transaction
|
|
||||||
.delete(customWidgetSecrets)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(customWidgetSecrets.definitionId, input.definitionId),
|
|
||||||
eq(customWidgetSecrets.sourceId, input.sourceId),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
}
|
|
||||||
for (const secret of secretRows) {
|
|
||||||
transaction
|
|
||||||
.delete(customWidgetSecrets)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(customWidgetSecrets.definitionId, input.definitionId),
|
|
||||||
eq(customWidgetSecrets.sourceId, input.sourceId),
|
|
||||||
eq(customWidgetSecrets.kind, secret.kind),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
transaction.insert(customWidgetSecrets).values(secret).run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
definitionId: input.definitionId,
|
definitionId: input.definitionId,
|
||||||
@@ -283,45 +170,7 @@ export const secretProcedures = {
|
|||||||
where: eq(customWidgetDefinitions.id, input.definitionId),
|
where: eq(customWidgetDefinitions.id, input.definitionId),
|
||||||
});
|
});
|
||||||
if (!existing) throw new TRPCError({ code: "NOT_FOUND" });
|
if (!existing) throw new TRPCError({ code: "NOT_FOUND" });
|
||||||
const updatedAt = new Date();
|
await clearCustomWidgetSecret(ctx.db, input);
|
||||||
await handleTransactionsAsync(ctx.db, {
|
|
||||||
async handleAsync(db, schema) {
|
|
||||||
await db.transaction(async (transaction) => {
|
|
||||||
await transaction
|
|
||||||
.delete(schema.customWidgetSecrets)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(schema.customWidgetSecrets.definitionId, input.definitionId),
|
|
||||||
eq(schema.customWidgetSecrets.sourceId, input.sourceId),
|
|
||||||
eq(schema.customWidgetSecrets.kind, input.kind),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await transaction
|
|
||||||
.update(schema.customWidgetDefinitions)
|
|
||||||
.set({ updatedAt })
|
|
||||||
.where(eq(schema.customWidgetDefinitions.id, input.definitionId));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handleSync(db) {
|
|
||||||
db.transaction((transaction) => {
|
|
||||||
transaction
|
|
||||||
.delete(customWidgetSecrets)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(customWidgetSecrets.definitionId, input.definitionId),
|
|
||||||
eq(customWidgetSecrets.sourceId, input.sourceId),
|
|
||||||
eq(customWidgetSecrets.kind, input.kind),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.run();
|
|
||||||
transaction
|
|
||||||
.update(customWidgetDefinitions)
|
|
||||||
.set({ updatedAt })
|
|
||||||
.where(eq(customWidgetDefinitions.id, input.definitionId))
|
|
||||||
.run();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return { sourceId: input.sourceId, kind: input.kind, isSet: false };
|
return { sourceId: input.sourceId, kind: input.kind, isSet: false };
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { createPinnedAgent } from "./network-policy";
|
||||||
|
|
||||||
|
const DISPATCHER_CLOSE_GRACE_MS = 1_000;
|
||||||
|
|
||||||
|
type CustomWidgetDispatcher = ReturnType<typeof createPinnedAgent>;
|
||||||
|
|
||||||
|
export async function closeDispatcher(
|
||||||
|
dispatcher: CustomWidgetDispatcher,
|
||||||
|
deadlineSignal: AbortSignal,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const cleanupController = new AbortController();
|
||||||
|
const timeout = setTimeout(() => cleanupController.abort(), DISPATCHER_CLOSE_GRACE_MS);
|
||||||
|
try {
|
||||||
|
await abortable(dispatcher.close(), AbortSignal.any([deadlineSignal, cleanupController.signal]));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
detachDispatcherDestroy(dispatcher);
|
||||||
|
if (cleanupController.signal.aborted && !deadlineSignal.aborted) return false;
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detachDispatcherDestroy(dispatcher: CustomWidgetDispatcher): void {
|
||||||
|
try {
|
||||||
|
void dispatcher.destroy().catch(() => undefined);
|
||||||
|
} catch {
|
||||||
|
// Cleanup is best effort after its bounded grace period.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function abortable<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
||||||
|
if (signal.aborted) return Promise.reject(signal.reason ?? createAbortError());
|
||||||
|
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const onAbort = () => {
|
||||||
|
signal.removeEventListener("abort", onAbort);
|
||||||
|
reject(signal.reason ?? createAbortError());
|
||||||
|
};
|
||||||
|
signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
operation.then(
|
||||||
|
(value) => {
|
||||||
|
signal.removeEventListener("abort", onAbort);
|
||||||
|
resolve(value);
|
||||||
|
},
|
||||||
|
(error: unknown) => {
|
||||||
|
signal.removeEventListener("abort", onAbort);
|
||||||
|
reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAbortError(): Error {
|
||||||
|
const error = new Error("Operation aborted");
|
||||||
|
error.name = "AbortError";
|
||||||
|
return error;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
resolveSameOriginTarget,
|
resolveSameOriginTarget,
|
||||||
validateCustomWidgetUrl,
|
validateCustomWidgetUrl,
|
||||||
} from "./network-policy";
|
} from "./network-policy";
|
||||||
|
import { closeDispatcher } from "./request-dispatcher-lifecycle";
|
||||||
import { parseResponseBody } from "./response";
|
import { parseResponseBody } from "./response";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -30,7 +31,6 @@ export {
|
|||||||
|
|
||||||
export const MAX_REQUEST_BODY_BYTES = 10 * 1024;
|
export const MAX_REQUEST_BODY_BYTES = 10 * 1024;
|
||||||
const REQUEST_TIMEOUT_MS = 10_000;
|
const REQUEST_TIMEOUT_MS = 10_000;
|
||||||
const DISPATCHER_CLOSE_GRACE_MS = 1_000;
|
|
||||||
const MAX_QUERY_REDIRECTS = 3;
|
const MAX_QUERY_REDIRECTS = 3;
|
||||||
export const MAX_REQUEST_DURATION_MS = 45_000;
|
export const MAX_REQUEST_DURATION_MS = 45_000;
|
||||||
const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"]);
|
const TIMEOUT_ERROR_CODES = new Set(["UND_ERR_CONNECT_TIMEOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"]);
|
||||||
@@ -213,60 +213,6 @@ async function performRequestWithinDeadline(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeDispatcher(
|
|
||||||
dispatcher: ReturnType<typeof createPinnedAgent>,
|
|
||||||
deadlineSignal: AbortSignal,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const cleanupController = new AbortController();
|
|
||||||
const timeout = setTimeout(() => cleanupController.abort(), DISPATCHER_CLOSE_GRACE_MS);
|
|
||||||
try {
|
|
||||||
await abortable(dispatcher.close(), AbortSignal.any([deadlineSignal, cleanupController.signal]));
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
detachDispatcherDestroy(dispatcher);
|
|
||||||
if (cleanupController.signal.aborted && !deadlineSignal.aborted) return false;
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function detachDispatcherDestroy(dispatcher: ReturnType<typeof createPinnedAgent>): void {
|
|
||||||
try {
|
|
||||||
void dispatcher.destroy().catch(() => undefined);
|
|
||||||
} catch {
|
|
||||||
// Cleanup is best effort after its bounded grace period.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function abortable<T>(operation: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
||||||
if (signal.aborted) return Promise.reject(signal.reason ?? createAbortError());
|
|
||||||
|
|
||||||
return new Promise<T>((resolve, reject) => {
|
|
||||||
const onAbort = () => {
|
|
||||||
signal.removeEventListener("abort", onAbort);
|
|
||||||
reject(signal.reason ?? createAbortError());
|
|
||||||
};
|
|
||||||
signal.addEventListener("abort", onAbort, { once: true });
|
|
||||||
operation.then(
|
|
||||||
(value) => {
|
|
||||||
signal.removeEventListener("abort", onAbort);
|
|
||||||
resolve(value);
|
|
||||||
},
|
|
||||||
(error: unknown) => {
|
|
||||||
signal.removeEventListener("abort", onAbort);
|
|
||||||
reject(error);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createAbortError(): Error {
|
|
||||||
const error = new Error("Operation aborted");
|
|
||||||
error.name = "AbortError";
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCustomWidgetRequestTimeoutError(error: unknown, ...signals: readonly AbortSignal[]): boolean {
|
export function isCustomWidgetRequestTimeoutError(error: unknown, ...signals: readonly AbortSignal[]): boolean {
|
||||||
if (signals.some((signal) => signal.aborted)) return true;
|
if (signals.some((signal) => signal.aborted)) return true;
|
||||||
if (!(error instanceof Error)) return false;
|
if (!(error instanceof Error)) return false;
|
||||||
|
|||||||
Reference in new issue
Block a user