mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-15 06:47:44 +02:00
687 lines
21 KiB
TypeScript
687 lines
21 KiB
TypeScript
import * as AWS from "@/AWS";
|
|
import * as Provider from "@/Provider";
|
|
import * as Test from "@/Test/Alchemy";
|
|
import * as iam from "@distilled.cloud/aws/iam";
|
|
import * as Lambda from "@distilled.cloud/aws/lambda";
|
|
import { expect } from "alchemy-test";
|
|
import * as Duration from "effect/Duration";
|
|
import * as Effect from "effect/Effect";
|
|
import * as Redacted from "effect/Redacted";
|
|
import * as Schedule from "effect/Schedule";
|
|
import * as Stream from "effect/Stream";
|
|
import * as HttpClient from "effect/http/HttpClient";
|
|
import { fileURLToPath } from "node:url";
|
|
import { TestFunction, TestFunctionLive } from "./handler.ts";
|
|
|
|
const timeoutHandlerPath = fileURLToPath(
|
|
new URL("./timeout-handler.ts", import.meta.url),
|
|
);
|
|
const externalPackageHandlerPath = fileURLToPath(
|
|
new URL("./external-package-handler.ts", import.meta.url),
|
|
);
|
|
const lockfilePinnedHandlerPath = (format: "npm" | "bun" | "pnpm" | "yarn") =>
|
|
fileURLToPath(
|
|
new URL(
|
|
`./fixtures/lockfile-pinning/${format}/handler.ts`,
|
|
import.meta.url,
|
|
),
|
|
);
|
|
|
|
const { test } = Test.make({ providers: AWS.providers() });
|
|
|
|
test.provider(
|
|
"create, update, delete function",
|
|
(stack) =>
|
|
Effect.gen(function* () {
|
|
yield* stack.destroy();
|
|
|
|
const deploy = (marker: string) =>
|
|
stack.deploy(
|
|
Effect.gen(function* () {
|
|
const fn = yield* TestFunction;
|
|
yield* fn.bind`ReadinessMarker`({
|
|
env: { READINESS_MARKER: marker },
|
|
});
|
|
return fn;
|
|
}).pipe(Effect.provide(TestFunctionLive)),
|
|
);
|
|
|
|
const { functionName, functionUrl, roleName } = yield* deploy("created");
|
|
yield* assertFunctionReady(functionName, "created");
|
|
|
|
const updated = yield* deploy("updated");
|
|
expect(updated.functionName).toBe(functionName);
|
|
yield* assertFunctionReady(updated.functionName, "updated");
|
|
|
|
expect(functionUrl).toBeTruthy();
|
|
|
|
const response = yield* HttpClient.get(functionUrl!).pipe(
|
|
Effect.flatMap((response) =>
|
|
response.status === 200
|
|
? Effect.succeed(response)
|
|
: Effect.fail(
|
|
new Error(`Function URL returned ${response.status}`),
|
|
),
|
|
),
|
|
Effect.tapError((error) => Effect.logError(error)),
|
|
Effect.retry({
|
|
schedule: Schedule.max([
|
|
Schedule.exponential(500),
|
|
Schedule.recurs(10),
|
|
]),
|
|
}),
|
|
);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(yield* response.text).toBe("Hello, world!");
|
|
|
|
const invokePolicy = yield* getPolicyStatement(
|
|
functionName,
|
|
"FunctionURLAllowPublicInvoke",
|
|
);
|
|
expect(invokePolicy.Condition).toEqual({
|
|
Bool: {
|
|
"lambda:InvokedViaFunctionUrl": "true",
|
|
},
|
|
});
|
|
|
|
yield* stack.destroy();
|
|
yield* assertFunctionDeleted(functionName);
|
|
yield* assertRoleDeleted(roleName);
|
|
|
|
const recreated = yield* deploy("recreated");
|
|
yield* assertFunctionReady(recreated.functionName, "recreated");
|
|
yield* stack.destroy();
|
|
yield* assertFunctionDeleted(recreated.functionName);
|
|
yield* assertRoleDeleted(recreated.roleName);
|
|
}).pipe(
|
|
Effect.tap(() => stack.destroy()),
|
|
Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
|
|
),
|
|
{
|
|
tags: ["provider:aws", "provider:aws:iam", "provider:aws:lambda", "live"],
|
|
timeout: 180_000,
|
|
},
|
|
);
|
|
|
|
test.provider(
|
|
"applies and updates the Lambda timeout",
|
|
(stack) =>
|
|
Effect.gen(function* () {
|
|
yield* stack.destroy();
|
|
|
|
const initial = yield* stack.deploy(
|
|
AWS.Lambda.Function("TimeoutFn", {
|
|
main: timeoutHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: false,
|
|
timeout: Duration.seconds(15),
|
|
}),
|
|
);
|
|
|
|
const initialConfig = yield* Lambda.getFunction({
|
|
FunctionName: initial.functionName,
|
|
});
|
|
expect(initialConfig.Configuration?.Timeout).toBe(15);
|
|
|
|
yield* stack.deploy(
|
|
AWS.Lambda.Function("TimeoutFn", {
|
|
main: timeoutHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: false,
|
|
timeout: Duration.seconds(45),
|
|
}),
|
|
);
|
|
|
|
const updatedConfig = yield* Lambda.getFunction({
|
|
FunctionName: initial.functionName,
|
|
}).pipe(
|
|
Effect.filterOrFail(
|
|
(c) => c.Configuration?.Timeout === 45,
|
|
() => new Error("Timeout update has not propagated yet"),
|
|
),
|
|
Effect.retry({
|
|
schedule: Schedule.max([
|
|
Schedule.exponential(500),
|
|
Schedule.recurs(10),
|
|
]),
|
|
}),
|
|
);
|
|
expect(updatedConfig.Configuration?.Timeout).toBe(45);
|
|
|
|
yield* stack.destroy();
|
|
yield* assertFunctionDeleted(initial.functionName);
|
|
}).pipe(
|
|
Effect.tap(() => stack.destroy()),
|
|
Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
|
|
),
|
|
{ tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 360_000 },
|
|
);
|
|
|
|
test.provider(
|
|
"installs explicit external packages into the deployment artifact",
|
|
(stack) =>
|
|
Effect.gen(function* () {
|
|
yield* stack.destroy();
|
|
|
|
const { functionName, functionUrl } = yield* stack.deploy(
|
|
AWS.Lambda.Function("InstallFn", {
|
|
main: externalPackageHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: true,
|
|
build: {
|
|
install: ["uuid"],
|
|
},
|
|
}),
|
|
);
|
|
|
|
const response = yield* HttpClient.get(functionUrl!).pipe(
|
|
Effect.flatMap((response) =>
|
|
response.status === 200
|
|
? Effect.succeed(response)
|
|
: Effect.fail(
|
|
new Error(`Function URL returned ${response.status}`),
|
|
),
|
|
),
|
|
Effect.retry({
|
|
schedule: Schedule.max([
|
|
Schedule.exponential(500),
|
|
Schedule.recurs(10),
|
|
]),
|
|
}),
|
|
);
|
|
|
|
const body = JSON.parse(yield* response.text) as { id: string };
|
|
expect(body.id).toMatch(
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
);
|
|
|
|
yield* stack.destroy();
|
|
yield* assertFunctionDeleted(functionName);
|
|
}).pipe(
|
|
Effect.tap(() => stack.destroy()),
|
|
Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
|
|
),
|
|
{ tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 360_000 },
|
|
);
|
|
|
|
test.provider(
|
|
"installs external packages at the versions pinned by each lockfile format",
|
|
(stack) =>
|
|
Effect.gen(function* () {
|
|
yield* stack.destroy();
|
|
|
|
// Each fixture project declares make-dir@^3.0.0 but its lockfile pins
|
|
// make-dir@3.0.0 and its transitive semver@6.3.0 — both below the latest
|
|
// versions satisfying their ranges (3.1.0 / 6.3.1). Free npm resolution
|
|
// installs the newer versions, so the assertions below fail without
|
|
// lockfile pinning. Legacy binary bun.lockb has no live fixture because
|
|
// current Bun can only write the text format; its print-and-parse path
|
|
// is covered by the yarn-v1 unit fixtures.
|
|
const formats = ["npm", "bun", "pnpm", "yarn"] as const;
|
|
const pinnedFunction = (format: (typeof formats)[number]) =>
|
|
AWS.Lambda.Function(`LockfilePinnedFn-${format}`, {
|
|
main: lockfilePinnedHandlerPath(format),
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: true,
|
|
build: {
|
|
install: ["make-dir"],
|
|
},
|
|
});
|
|
const deployed = yield* stack.deploy(
|
|
Effect.all({
|
|
npm: pinnedFunction("npm"),
|
|
bun: pinnedFunction("bun"),
|
|
pnpm: pinnedFunction("pnpm"),
|
|
yarn: pinnedFunction("yarn"),
|
|
}),
|
|
);
|
|
|
|
for (const format of formats) {
|
|
const { functionUrl } = deployed[format];
|
|
const response = yield* HttpClient.get(functionUrl!).pipe(
|
|
Effect.flatMap((response) =>
|
|
response.status === 200
|
|
? Effect.succeed(response)
|
|
: Effect.fail(
|
|
new Error(`Function URL returned ${response.status}`),
|
|
),
|
|
),
|
|
Effect.retry({
|
|
schedule: Schedule.max([
|
|
Schedule.exponential(500),
|
|
Schedule.recurs(10),
|
|
]),
|
|
}),
|
|
);
|
|
|
|
const body = JSON.parse(yield* response.text) as {
|
|
makeDir: string;
|
|
semver: string;
|
|
};
|
|
expect(body.makeDir, format).toBe("3.0.0");
|
|
expect(body.semver, format).toBe("6.3.0");
|
|
}
|
|
|
|
yield* stack.destroy();
|
|
yield* assertFunctionDeleted(deployed.npm.functionName);
|
|
}).pipe(
|
|
Effect.tap(() => stack.destroy()),
|
|
Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
|
|
),
|
|
{ tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 360_000 },
|
|
);
|
|
|
|
test.provider(
|
|
"applies and updates the Lambda architecture",
|
|
(stack) =>
|
|
Effect.gen(function* () {
|
|
yield* stack.destroy();
|
|
|
|
const initial = yield* stack.deploy(
|
|
AWS.Lambda.Function("ArchitectureFn", {
|
|
main: timeoutHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: false,
|
|
architecture: "arm64",
|
|
}),
|
|
);
|
|
|
|
yield* waitForArchitecture(initial.functionName, "arm64");
|
|
|
|
const updated = yield* stack.deploy(
|
|
AWS.Lambda.Function("ArchitectureFn", {
|
|
main: timeoutHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: false,
|
|
}),
|
|
);
|
|
|
|
expect(updated.functionName).toBe(initial.functionName);
|
|
yield* waitForArchitecture(updated.functionName, "x86_64");
|
|
|
|
yield* stack.destroy();
|
|
yield* assertFunctionDeleted(initial.functionName);
|
|
}).pipe(
|
|
Effect.tap(() => stack.destroy()),
|
|
Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
|
|
),
|
|
{ tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 360_000 },
|
|
);
|
|
|
|
test.provider(
|
|
"applies, updates, and removes reserved concurrency",
|
|
(stack) =>
|
|
Effect.gen(function* () {
|
|
yield* stack.destroy();
|
|
|
|
const initial = yield* stack.deploy(
|
|
AWS.Lambda.Function("ConcurrencyFn", {
|
|
main: timeoutHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: false,
|
|
}),
|
|
);
|
|
|
|
expect(initial.reservedConcurrentExecutions).toBeUndefined();
|
|
yield* waitForReservedConcurrency(initial.functionName, undefined);
|
|
|
|
const updated = yield* stack.deploy(
|
|
AWS.Lambda.Function("ConcurrencyFn", {
|
|
main: timeoutHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: false,
|
|
reservedConcurrentExecutions: 0,
|
|
}),
|
|
);
|
|
|
|
expect(updated.functionName).toBe(initial.functionName);
|
|
expect(updated.reservedConcurrentExecutions).toBe(0);
|
|
yield* waitForReservedConcurrency(updated.functionName, 0);
|
|
|
|
const removed = yield* stack.deploy(
|
|
AWS.Lambda.Function("ConcurrencyFn", {
|
|
main: timeoutHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: false,
|
|
}),
|
|
);
|
|
|
|
expect(removed.functionName).toBe(initial.functionName);
|
|
expect(removed.reservedConcurrentExecutions).toBeUndefined();
|
|
yield* waitForReservedConcurrency(removed.functionName, undefined);
|
|
|
|
yield* stack.destroy();
|
|
yield* assertFunctionDeleted(initial.functionName);
|
|
}).pipe(
|
|
Effect.tap(() => stack.destroy()),
|
|
Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
|
|
),
|
|
{ tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 360_000 },
|
|
);
|
|
|
|
// Canonical `list()` test (AWS account/region-scoped collection): deploy a
|
|
// real function, resolve the provider from context via the typed
|
|
// `Provider.findProvider`, call `list()`, and assert the deployed function
|
|
// appears in the exhaustively-paginated result.
|
|
test.provider(
|
|
"list enumerates the deployed function",
|
|
(stack) =>
|
|
Effect.gen(function* () {
|
|
yield* stack.destroy();
|
|
|
|
const deployed = yield* stack.deploy(
|
|
AWS.Lambda.Function("ListFn", {
|
|
main: timeoutHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: false,
|
|
}),
|
|
);
|
|
|
|
const provider = yield* Provider.findProvider(AWS.Lambda.Function);
|
|
const all = yield* provider.list();
|
|
|
|
expect(all.some((f) => f.functionName === deployed.functionName)).toBe(
|
|
true,
|
|
);
|
|
|
|
yield* stack.destroy();
|
|
yield* assertFunctionDeleted(deployed.functionName);
|
|
}).pipe(
|
|
Effect.tap(() => stack.destroy()),
|
|
Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
|
|
),
|
|
{ tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 180_000 },
|
|
);
|
|
|
|
test.provider(
|
|
"updates function URL auth to AWS_IAM",
|
|
(stack) =>
|
|
Effect.gen(function* () {
|
|
yield* stack.destroy();
|
|
|
|
const initial = yield* stack.deploy(
|
|
AWS.Lambda.Function("IamUrlFn", {
|
|
main: timeoutHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: true,
|
|
}),
|
|
);
|
|
|
|
const invokePolicy = yield* getPolicyStatement(
|
|
initial.functionName,
|
|
"FunctionURLAllowPublicInvoke",
|
|
);
|
|
expect(invokePolicy.Condition).toEqual({
|
|
Bool: {
|
|
"lambda:InvokedViaFunctionUrl": "true",
|
|
},
|
|
});
|
|
|
|
const updated = yield* stack.deploy(
|
|
AWS.Lambda.Function("IamUrlFn", {
|
|
main: timeoutHandlerPath,
|
|
handler: "handler",
|
|
isExternal: true,
|
|
functionUrl: {
|
|
authType: "AWS_IAM",
|
|
cors: {
|
|
AllowHeaders: ["authorization", "content-type"],
|
|
AllowMethods: ["GET", "POST"],
|
|
AllowOrigins: ["https://example.com"],
|
|
ExposeHeaders: ["x-request-id"],
|
|
MaxAge: 300,
|
|
},
|
|
invokeMode: "RESPONSE_STREAM",
|
|
},
|
|
}),
|
|
);
|
|
|
|
expect(updated.functionName).toBe(initial.functionName);
|
|
expect(updated.functionUrl).toBeTruthy();
|
|
|
|
const config = yield* getFunctionUrlConfigWithAuth(
|
|
updated.functionName,
|
|
"AWS_IAM",
|
|
);
|
|
expect(config.AuthType).toBe("AWS_IAM");
|
|
expect(config.InvokeMode).toBe("RESPONSE_STREAM");
|
|
expect(config.Cors).toEqual({
|
|
AllowHeaders: ["authorization", "content-type"],
|
|
AllowMethods: ["GET", "POST"],
|
|
AllowOrigins: ["https://example.com"],
|
|
ExposeHeaders: ["x-request-id"],
|
|
MaxAge: 300,
|
|
});
|
|
|
|
yield* waitForPolicyStatementAbsent(
|
|
updated.functionName,
|
|
"FunctionURLAllowPublicAccess",
|
|
);
|
|
yield* waitForPolicyStatementAbsent(
|
|
updated.functionName,
|
|
"FunctionURLAllowPublicInvoke",
|
|
);
|
|
|
|
const response = yield* HttpClient.get(updated.functionUrl!).pipe(
|
|
Effect.flatMap((response) =>
|
|
response.status === 403
|
|
? Effect.succeed(response)
|
|
: Effect.fail(
|
|
new Error(`IAM Function URL returned ${response.status}`),
|
|
),
|
|
),
|
|
Effect.retry({
|
|
schedule: Schedule.max([
|
|
Schedule.exponential(500),
|
|
Schedule.recurs(10),
|
|
]),
|
|
}),
|
|
);
|
|
expect(response.status).toBe(403);
|
|
|
|
yield* stack.destroy();
|
|
yield* assertFunctionDeleted(initial.functionName);
|
|
}).pipe(
|
|
Effect.tap(() => stack.destroy()),
|
|
Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
|
|
),
|
|
{ tags: ["provider:aws", "provider:aws:lambda", "live"], timeout: 360_000 },
|
|
);
|
|
|
|
const assertFunctionReady = Effect.fn(function* (
|
|
functionName: string,
|
|
marker: string,
|
|
) {
|
|
// Deploy must finish configuration propagation; these checks never retry.
|
|
const { Configuration } = yield* Lambda.getFunction({
|
|
FunctionName: functionName,
|
|
});
|
|
expect(Configuration?.State).toBe("Active");
|
|
expect(Configuration?.LastUpdateStatus).toBe("Successful");
|
|
const observed = Configuration?.Environment?.Variables?.READINESS_MARKER;
|
|
expect(
|
|
Redacted.isRedacted(observed) ? Redacted.value(observed) : observed,
|
|
).toBe(marker);
|
|
|
|
const response = yield* Lambda.invoke({
|
|
FunctionName: functionName,
|
|
Payload: JSON.stringify({
|
|
version: "2.0",
|
|
rawPath: "/readiness",
|
|
rawQueryString: "",
|
|
headers: { host: "localhost" },
|
|
requestContext: {
|
|
http: {
|
|
method: "GET",
|
|
path: "/readiness",
|
|
protocol: "HTTP/1.1",
|
|
sourceIp: "127.0.0.1",
|
|
userAgent: "alchemy-test",
|
|
},
|
|
},
|
|
isBase64Encoded: false,
|
|
}),
|
|
});
|
|
expect(response.FunctionError).toBeUndefined();
|
|
const payload = response.Payload
|
|
? yield* response.Payload.pipe(Stream.decodeText(), Stream.mkString)
|
|
: "";
|
|
const body = yield* Effect.try(() => JSON.parse(payload));
|
|
expect(body.statusCode).toBe(200);
|
|
expect(body.body).toBe(marker);
|
|
});
|
|
|
|
// Out-of-band proof that the trailing destroy actually removed the function
|
|
// from the cloud (bounded retry to ride out delete propagation).
|
|
const assertFunctionDeleted = Effect.fn(function* (functionName: string) {
|
|
yield* Lambda.getFunction({ FunctionName: functionName }).pipe(
|
|
Effect.flatMap(() =>
|
|
Effect.fail(new Error(`Function ${functionName} still exists`)),
|
|
),
|
|
Effect.catchTag("ResourceNotFoundException", () => Effect.void),
|
|
Effect.retry({
|
|
schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
// Function cleanup owns its generated execution role. Verify the role is not
|
|
// stranded when deletion also waits for Lambda's asynchronous log flush.
|
|
const assertRoleDeleted = Effect.fn(function* (roleName: string) {
|
|
yield* iam.getRole({ RoleName: roleName }).pipe(
|
|
Effect.flatMap(() =>
|
|
Effect.fail(new Error(`Role ${roleName} still exists`)),
|
|
),
|
|
Effect.catchTag("NoSuchEntityException", () => Effect.void),
|
|
Effect.retry({
|
|
schedule: Schedule.spaced("1 second"),
|
|
times: 8,
|
|
}),
|
|
);
|
|
});
|
|
|
|
const getPolicyStatement = Effect.fn(function* (
|
|
functionName: string,
|
|
statementId: string,
|
|
) {
|
|
return yield* findPolicyStatement(functionName, statementId).pipe(
|
|
Effect.flatMap((statement) =>
|
|
statement
|
|
? Effect.succeed(statement)
|
|
: Effect.fail(new Error(`Policy statement ${statementId} not found`)),
|
|
),
|
|
Effect.retry({
|
|
schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(10)]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
const waitForPolicyStatementAbsent = Effect.fn(function* (
|
|
functionName: string,
|
|
statementId: string,
|
|
) {
|
|
yield* findPolicyStatement(functionName, statementId).pipe(
|
|
Effect.flatMap((statement) =>
|
|
statement
|
|
? Effect.fail(new Error(`Policy statement ${statementId} still exists`))
|
|
: Effect.void,
|
|
),
|
|
Effect.retry({
|
|
schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(10)]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
const findPolicyStatement = Effect.fn(function* (
|
|
functionName: string,
|
|
statementId: string,
|
|
) {
|
|
return yield* Lambda.getPolicy({ FunctionName: functionName }).pipe(
|
|
Effect.flatMap(({ Policy }) =>
|
|
Effect.try({
|
|
try: () => {
|
|
const policy = JSON.parse(Policy ?? "{}") as {
|
|
Statement?: Array<{
|
|
Sid?: string;
|
|
Condition?: unknown;
|
|
}>;
|
|
};
|
|
return policy.Statement?.find(
|
|
(statement) => statement.Sid === statementId,
|
|
);
|
|
},
|
|
catch: (cause) =>
|
|
cause instanceof Error ? cause : new Error(String(cause)),
|
|
}),
|
|
),
|
|
Effect.catchTag("ResourceNotFoundException", () =>
|
|
Effect.succeed(undefined),
|
|
),
|
|
);
|
|
});
|
|
|
|
const getFunctionUrlConfigWithAuth = Effect.fn(function* (
|
|
functionName: string,
|
|
authType: Lambda.FunctionUrlAuthType,
|
|
) {
|
|
return yield* Lambda.getFunctionUrlConfig({
|
|
FunctionName: functionName,
|
|
}).pipe(
|
|
Effect.filterOrFail(
|
|
(config) => config.AuthType === authType,
|
|
() => new Error("Function URL auth has not propagated yet"),
|
|
),
|
|
Effect.retry({
|
|
schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(10)]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
const waitForReservedConcurrency = Effect.fn(function* (
|
|
functionName: string,
|
|
expected: number | undefined,
|
|
) {
|
|
return yield* Lambda.getFunctionConcurrency({
|
|
FunctionName: functionName,
|
|
}).pipe(
|
|
Effect.map((config) => config.ReservedConcurrentExecutions),
|
|
Effect.catchTag("ResourceNotFoundException", () =>
|
|
Effect.succeed(undefined),
|
|
),
|
|
Effect.filterOrFail(
|
|
(actual) => actual === expected,
|
|
() => new Error("Reserved concurrency update has not propagated yet"),
|
|
),
|
|
Effect.retry({
|
|
schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(10)]),
|
|
}),
|
|
);
|
|
});
|
|
|
|
const waitForArchitecture = Effect.fn(function* (
|
|
functionName: string,
|
|
expected: AWS.Lambda.FunctionArchitecture,
|
|
) {
|
|
return yield* Lambda.getFunction({ FunctionName: functionName }).pipe(
|
|
Effect.map((result) => result.Configuration?.Architectures),
|
|
Effect.filterOrFail(
|
|
(architectures) => architectures?.[0] === expected,
|
|
() => new Error("Lambda architecture update has not propagated yet"),
|
|
),
|
|
Effect.retry({
|
|
schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(10)]),
|
|
}),
|
|
);
|
|
});
|