add runSync safety comment
This commit is contained in:
@@ -166,6 +166,8 @@ export namespace Bus {
|
||||
|
||||
const { runPromise, runSync } = makeRuntime(Service, layer)
|
||||
|
||||
// runSync is safe here because the subscribe chain (InstanceState.get, PubSub.subscribe,
|
||||
// Scope.make, Effect.forkScoped) is entirely synchronous. If any step becomes async, this will throw.
|
||||
export async function publish<D extends BusEvent.Definition>(def: D, properties: z.output<D["properties"]>) {
|
||||
return runPromise((svc) => svc.publish(def, properties))
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import z from "zod"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { BusEvent } from "../../src/bus/bus-event"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { provideInstance } from "../fixture/instance"
|
||||
import { provideInstance, tmpdir } from "../fixture/fixture"
|
||||
|
||||
const TestEvent = {
|
||||
Ping: BusEvent.define("test.effect.ping", z.object({ value: z.number() })),
|
||||
|
||||
@@ -2,9 +2,8 @@ import { $ } from "bun"
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Deferred, Effect, Option } from "effect"
|
||||
import { ConfigProvider, Deferred, Effect, Layer, ManagedRuntime, Option } from "effect"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { watcherConfigLayer, withServices } from "../fixture/instance"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { FileWatcher } from "../../src/file/watcher"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
@@ -16,20 +15,33 @@ const describeWatcher = FileWatcher.hasNativeBinding() && !process.env.CI ? desc
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const watcherConfigLayer = ConfigProvider.layer(
|
||||
ConfigProvider.fromUnknown({
|
||||
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
|
||||
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "false",
|
||||
}),
|
||||
)
|
||||
|
||||
type WatcherEvent = { file: string; event: "add" | "change" | "unlink" }
|
||||
|
||||
/** Run `body` with a live FileWatcher service. */
|
||||
function withWatcher<E>(directory: string, body: Effect.Effect<void, E>) {
|
||||
return withServices(
|
||||
return Instance.provide({
|
||||
directory,
|
||||
FileWatcher.layer,
|
||||
async (rt) => {
|
||||
await rt.runPromise(FileWatcher.Service.use((s) => s.init()))
|
||||
await Effect.runPromise(ready(directory))
|
||||
await Effect.runPromise(body)
|
||||
fn: async () => {
|
||||
const layer: Layer.Layer<FileWatcher.Service, never, never> = FileWatcher.layer.pipe(
|
||||
Layer.provide(watcherConfigLayer),
|
||||
)
|
||||
const rt = ManagedRuntime.make(layer)
|
||||
try {
|
||||
await rt.runPromise(FileWatcher.Service.use((s) => s.init()))
|
||||
await Effect.runPromise(ready(directory))
|
||||
await Effect.runPromise(body)
|
||||
} finally {
|
||||
await rt.dispose()
|
||||
}
|
||||
},
|
||||
{ provide: [watcherConfigLayer] },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
function listen(directory: string, check: (evt: WatcherEvent) => boolean, hit: (evt: WatcherEvent) => void) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from "path"
|
||||
import { Effect, FileSystem } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import type { Config } from "../../src/config/config"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
|
||||
// Strip null bytes from paths (defensive fix for CI environment issues)
|
||||
function sanitizePath(p: string): string {
|
||||
@@ -81,11 +82,10 @@ export function tmpdirScoped(options?: { git?: boolean; config?: Partial<Config.
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "opencode-test-" })
|
||||
|
||||
function git(...args: string[]) {
|
||||
return spawner.spawn(ChildProcess.make("git", args, { cwd: dir })).pipe(
|
||||
Effect.flatMap((handle) => handle.exitCode),
|
||||
)
|
||||
}
|
||||
const git = (...args: string[]) =>
|
||||
spawner
|
||||
.spawn(ChildProcess.make("git", args, { cwd: dir }))
|
||||
.pipe(Effect.flatMap((handle) => handle.exitCode))
|
||||
|
||||
if (options?.git) {
|
||||
yield* git("init")
|
||||
@@ -105,3 +105,22 @@ export function tmpdirScoped(options?: { git?: boolean; config?: Partial<Config.
|
||||
return dir
|
||||
})
|
||||
}
|
||||
|
||||
export const provideInstance =
|
||||
(directory: string) =>
|
||||
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
||||
Effect.withFiber((fiber) =>
|
||||
Effect.promise<A>(async () =>
|
||||
Instance.provide({
|
||||
directory,
|
||||
fn: () => Effect.runPromiseWith(fiber.services as any)(self),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
export function tmpdirInstanceScoped(options?: { git?: boolean; config?: Partial<Config.Info> }) {
|
||||
return Effect.map(tmpdirScoped(options), (path) => ({
|
||||
path,
|
||||
provide: provideInstance(path),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -43,12 +43,12 @@ export function withServices<S>(
|
||||
|
||||
export const provideInstance =
|
||||
(directory: string) =>
|
||||
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
||||
Effect.withFiber((fiber) =>
|
||||
<A, E = never, R = never>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
||||
Effect.services<A, E, R>((fiber) =>
|
||||
Effect.promise<A>(async () =>
|
||||
Instance.provide({
|
||||
directory,
|
||||
fn: () => Effect.runPromiseWith(fiber.services as any)(self),
|
||||
fn: () => Effect.runPromiseWith(fiber.services)(self),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
import { Effect } from "effect"
|
||||
import { Effect, Layer, ManagedRuntime } from "effect"
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { withServices } from "../fixture/instance"
|
||||
import { Bus } from "../../src/bus"
|
||||
import { File } from "../../src/file"
|
||||
import { Format } from "../../src/format"
|
||||
import * as Formatter from "../../src/format/formatter"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
|
||||
function withRuntime<S, E>(
|
||||
directory: string,
|
||||
layer: Layer.Layer<S, E, never>,
|
||||
body: (rt: ManagedRuntime.ManagedRuntime<S, E>) => Promise<void>,
|
||||
) {
|
||||
return Instance.provide({
|
||||
directory,
|
||||
fn: async () => {
|
||||
const rt = ManagedRuntime.make(layer)
|
||||
try {
|
||||
await body(rt)
|
||||
} finally {
|
||||
await rt.dispose()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe("Format", () => {
|
||||
afterEach(async () => {
|
||||
await Instance.disposeAll()
|
||||
@@ -16,7 +31,7 @@ describe("Format", () => {
|
||||
test("status() returns built-in formatters when no config overrides", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await withServices(tmp.path, Format.layer, async (rt) => {
|
||||
await withRuntime(tmp.path, Format.layer, async (rt) => {
|
||||
const statuses = await rt.runPromise(Format.Service.use((s) => s.status()))
|
||||
expect(Array.isArray(statuses)).toBe(true)
|
||||
expect(statuses.length).toBeGreaterThan(0)
|
||||
@@ -38,7 +53,7 @@ describe("Format", () => {
|
||||
config: { formatter: false },
|
||||
})
|
||||
|
||||
await withServices(tmp.path, Format.layer, async (rt) => {
|
||||
await withRuntime(tmp.path, Format.layer, async (rt) => {
|
||||
const statuses = await rt.runPromise(Format.Service.use((s) => s.status()))
|
||||
expect(statuses).toEqual([])
|
||||
})
|
||||
@@ -53,7 +68,7 @@ describe("Format", () => {
|
||||
},
|
||||
})
|
||||
|
||||
await withServices(tmp.path, Format.layer, async (rt) => {
|
||||
await withRuntime(tmp.path, Format.layer, async (rt) => {
|
||||
const statuses = await rt.runPromise(Format.Service.use((s) => s.status()))
|
||||
const gofmt = statuses.find((s) => s.name === "gofmt")
|
||||
expect(gofmt).toBeUndefined()
|
||||
@@ -63,7 +78,7 @@ describe("Format", () => {
|
||||
test("service initializes without error", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await withServices(tmp.path, Format.layer, async (rt) => {
|
||||
await withRuntime(tmp.path, Format.layer, async (rt) => {
|
||||
await rt.runPromise(Format.Service.use(() => Effect.void))
|
||||
})
|
||||
})
|
||||
@@ -127,7 +142,7 @@ describe("Format", () => {
|
||||
}
|
||||
|
||||
try {
|
||||
await withServices(tmp.path, Format.layer, async (rt) => {
|
||||
await withRuntime(tmp.path, Format.layer, async (rt) => {
|
||||
await rt.runPromise(Format.Service.use((s) => s.init()))
|
||||
await rt.runPromise(Format.Service.use((s) => s.file(file)))
|
||||
})
|
||||
@@ -162,7 +177,7 @@ describe("Format", () => {
|
||||
const file = `${tmp.path}/test.seq`
|
||||
await Bun.write(file, "x")
|
||||
|
||||
await withServices(tmp.path, Format.layer, async (rt) => {
|
||||
await withRuntime(tmp.path, Format.layer, async (rt) => {
|
||||
await rt.runPromise(Format.Service.use((s) => s.init()))
|
||||
await rt.runPromise(Format.Service.use((s) => s.file(file)))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user