Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .oxlintrc.effect.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
},
"ignorePatterns": [
"**",
"!packages/api/src/**",
"!packages/config/**",
"!packages/stack/**",
"!apps/cli/src/auth/**",
Expand All @@ -23,6 +24,7 @@
"apps/cli/src/shared/cli/bin.ts",
"apps/cli/src/shared/compute/stacks/**",
"apps/cli/src/shared/feedback/database.types.ts",
"packages/api/src/generated/contracts.ts",
"!apps/cli/tests/helpers/branches-live.ts",
"!apps/cli/tests/helpers/compute.ts",
"!apps/cli/tests/helpers/migration-live.ts",
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/command-internal/http-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export function mapHttpError<N, S>(opts: {
if (cause instanceof SupabaseApiInputError || cause instanceof HttpBody.HttpBodyError) {
// Client-side validation/build failures: keep their identity, since
// this generic mapper can't safely reclassify them as response errors.
return yield* Effect.fail(cause);
return yield* cause;
}
if (HttpClientError.isHttpClientError(cause)) {
if (RESPONSE_ERROR_TAGS.has(cause.reason._tag) && cause.response !== undefined) {
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/commands/start/start.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1812,7 +1812,7 @@ export const start = Effect.fn("start")(function* (flags: StartFlags) {
if (Result.isFailure(seedResult)) {
// No manual `rollbackStart` here — the outer
// `Effect.onError` below rolls back on this failure too.
return yield* Effect.fail(seedResult.failure);
return yield* seedResult.failure;
}
}
}
Expand Down
39 changes: 24 additions & 15 deletions packages/api/src/bun-lifecycle.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { describe, expect, test } from "vitest";
import { Effect } from "effect";
import { createApiClient } from "./bun.ts";

describe("Bun client lifecycle", () => {
test("serves requests through the Bun entrypoint and rejects operations after disposal", async () => {
test("serves requests through the Bun entrypoint and rejects operations after disposal", () => {
const paths: string[] = [];
const server = Bun.serve({
hostname: "127.0.0.1",
Expand All @@ -12,19 +13,27 @@ describe("Bun client lifecycle", () => {
return Response.json([]);
},
});
const client = await createApiClient({
baseUrl: server.url.toString(),
accessToken: "test-token",
});
try {
await expect(client.v1.listAllProjects()).resolves.toEqual([]);
expect(paths).toEqual(["/v1/projects"]);
await client.dispose();
await expect(client.v1.listAllProjects()).rejects.toThrow();
expect(paths).toEqual(["/v1/projects"]);
} finally {
await client.dispose();
await server.stop(true);
}
return Effect.runPromise(
Effect.acquireUseRelease(
Effect.promise(() =>
createApiClient({
baseUrl: server.url.toString(),
accessToken: "test-token",
}),
),
(client) =>
Effect.gen(function* () {
yield* Effect.promise(() => expect(client.v1.listAllProjects()).resolves.toEqual([]));
expect(paths).toEqual(["/v1/projects"]);
yield* Effect.promise(() => client.dispose());
yield* Effect.promise(() => expect(client.v1.listAllProjects()).rejects.toThrow());
expect(paths).toEqual(["/v1/projects"]);
}),
(client) =>
Effect.promise(() => client.dispose()).pipe(
Effect.andThen(Effect.promise(() => server.stop(true))),
),
),
);
});
});
13 changes: 5 additions & 8 deletions packages/api/src/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@ import { type SupabaseApiClientOptions, type SupabaseApiConfig } from "./interna
import { makePromiseClient, type PromiseClient } from "./internal/promise-client.ts";

/** Creates a Promise API client backed by a managed Bun runtime. */
export async function createApiClient(
export function createApiClient(
config: SupabaseApiConfig = {},
options?: SupabaseApiClientOptions,
): Promise<PromiseSupabaseApiClient> {
const runtime = ManagedRuntime.make(Layer.mergeAll(BunServices.layer, FetchHttpClient.layer));
try {
const effectClient = await runtime.runPromise(makeApiClient(config, options));
return makePromiseClient(runtime, effectClient);
} catch (error) {
await runtime.dispose();
throw error;
}
return runtime
.runPromise(makeApiClient(config, options))
.then((effectClient) => makePromiseClient(runtime, effectClient))
.catch((error: unknown) => runtime.dispose().then(() => Promise.reject(error)));
}

/** Promise API client whose runtime resources are released by `dispose`. */
Expand Down
36 changes: 19 additions & 17 deletions packages/api/src/config/api-config.layer.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import { describe, expect, test } from "vitest";
import { ConfigProvider, Effect, Option } from "effect";
import { ConfigProvider, Effect, Layer, Option } from "effect";

import { apiConfigLayer, DEFAULT_SUPABASE_API_URL } from "./api-config.layer.ts";
import { ApiConfig } from "./api-config.service.ts";

describe("apiConfigLayer", () => {
test("defaults the API URL and reads the access token from config", async () => {
const config = await Effect.runPromise(
test("defaults the API URL and reads the access token from config", () =>
Effect.runPromise(
Effect.gen(function* () {
return yield* ApiConfig;
}).pipe(
Effect.provide(apiConfigLayer),
Effect.provide(
ConfigProvider.layer(
ConfigProvider.fromUnknown({
SUPABASE_ACCESS_TOKEN: "env-token",
}),
const config = yield* Effect.service(ApiConfig).pipe(
Effect.provide(
apiConfigLayer.pipe(
Layer.provide(
ConfigProvider.layer(
ConfigProvider.fromUnknown({
SUPABASE_ACCESS_TOKEN: "env-token",
}),
),
),
),
),
),
),
);
);

expect(config.baseUrl).toBe(DEFAULT_SUPABASE_API_URL);
expect(Option.isSome(config.accessToken)).toBe(true);
});
expect(config.baseUrl).toBe(DEFAULT_SUPABASE_API_URL);
expect(Option.isSome(config.accessToken)).toBe(true);
}),
));
});
Loading
Loading