refactor: unwrap Workspace namespace + self-reexport (#22934)

This commit is contained in:
Kit Langton
2026-04-17 00:00:15 +00:00
committed by GitHub
parent f9aa3d77cd
commit bae80af1b4
@@ -26,27 +26,26 @@ import { AppRuntime } from "@/effect/app-runtime"
import { EventSequenceTable } from "@/sync/event.sql" import { EventSequenceTable } from "@/sync/event.sql"
import { waitEvent } from "./util" import { waitEvent } from "./util"
export namespace Workspace { export const Info = WorkspaceInfo.meta({
export const Info = WorkspaceInfo.meta({
ref: "Workspace", ref: "Workspace",
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
export const ConnectionStatus = z.object({ export const ConnectionStatus = z.object({
workspaceID: WorkspaceID.zod, workspaceID: WorkspaceID.zod,
status: z.enum(["connected", "connecting", "disconnected", "error"]), status: z.enum(["connected", "connecting", "disconnected", "error"]),
error: z.string().optional(), error: z.string().optional(),
}) })
export type ConnectionStatus = z.infer<typeof ConnectionStatus> export type ConnectionStatus = z.infer<typeof ConnectionStatus>
const Restore = z.object({ const Restore = z.object({
workspaceID: WorkspaceID.zod, workspaceID: WorkspaceID.zod,
sessionID: SessionID.zod, sessionID: SessionID.zod,
total: z.number().int().min(0), total: z.number().int().min(0),
step: z.number().int().min(0), step: z.number().int().min(0),
}) })
export const Event = { export const Event = {
Ready: BusEvent.define( Ready: BusEvent.define(
"workspace.ready", "workspace.ready",
z.object({ z.object({
@@ -61,9 +60,9 @@ export namespace Workspace {
), ),
Restore: BusEvent.define("workspace.restore", Restore), Restore: BusEvent.define("workspace.restore", Restore),
Status: BusEvent.define("workspace.status", ConnectionStatus), Status: BusEvent.define("workspace.status", ConnectionStatus),
} }
function fromRow(row: typeof WorkspaceTable.$inferSelect): Info { function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
return { return {
id: row.id, id: row.id,
type: row.type, type: row.type,
@@ -73,17 +72,17 @@ export namespace Workspace {
extra: row.extra, extra: row.extra,
projectID: row.project_id, projectID: row.project_id,
} }
} }
const CreateInput = z.object({ const CreateInput = z.object({
id: WorkspaceID.zod.optional(), id: WorkspaceID.zod.optional(),
type: Info.shape.type, type: Info.shape.type,
branch: Info.shape.branch, branch: Info.shape.branch,
projectID: ProjectID.zod, projectID: ProjectID.zod,
extra: Info.shape.extra, extra: Info.shape.extra,
}) })
export const create = fn(CreateInput, async (input) => { export const create = fn(CreateInput, async (input) => {
const id = WorkspaceID.ascending(input.id) const id = WorkspaceID.ascending(input.id)
const adaptor = await getAdaptor(input.projectID, input.type) const adaptor = await getAdaptor(input.projectID, input.type)
@@ -134,14 +133,14 @@ export namespace Workspace {
}) })
return info return info
}) })
const SessionRestoreInput = z.object({ const SessionRestoreInput = z.object({
workspaceID: WorkspaceID.zod, workspaceID: WorkspaceID.zod,
sessionID: SessionID.zod, sessionID: SessionID.zod,
}) })
export const sessionRestore = fn(SessionRestoreInput, async (input) => { export const sessionRestore = fn(SessionRestoreInput, async (input) => {
log.info("session restore requested", { log.info("session restore requested", {
workspaceID: input.workspaceID, workspaceID: input.workspaceID,
sessionID: input.sessionID, sessionID: input.sessionID,
@@ -292,9 +291,9 @@ export namespace Workspace {
}) })
throw err throw err
} }
}) })
export function list(project: Project.Info) { export function list(project: Project.Info) {
const rows = Database.use((db) => const rows = Database.use((db) =>
db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(), db.select().from(WorkspaceTable).where(eq(WorkspaceTable.project_id, project.id)).all(),
) )
@@ -302,22 +301,22 @@ export namespace Workspace {
for (const space of spaces) startSync(space) for (const space of spaces) startSync(space)
return spaces return spaces
} }
function lookup(id: WorkspaceID) { function lookup(id: WorkspaceID) {
const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get()) const row = Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get())
if (!row) return if (!row) return
return fromRow(row) return fromRow(row)
} }
export const get = fn(WorkspaceID.zod, async (id) => { export const get = fn(WorkspaceID.zod, async (id) => {
const space = lookup(id) const space = lookup(id)
if (!space) return if (!space) return
startSync(space) startSync(space)
return space return space
}) })
export const remove = fn(WorkspaceID.zod, async (id) => { export const remove = fn(WorkspaceID.zod, async (id) => {
const sessions = Database.use((db) => const sessions = Database.use((db) =>
db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(), db.select({ id: SessionTable.id }).from(SessionTable).where(eq(SessionTable.workspace_id, id)).all(),
) )
@@ -340,13 +339,13 @@ export namespace Workspace {
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run()) Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
return info return info
} }
}) })
const connections = new Map<WorkspaceID, ConnectionStatus>() const connections = new Map<WorkspaceID, ConnectionStatus>()
const aborts = new Map<WorkspaceID, AbortController>() const aborts = new Map<WorkspaceID, AbortController>()
const TIMEOUT = 5000 const TIMEOUT = 5000
function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) { function setStatus(id: WorkspaceID, status: ConnectionStatus["status"], error?: string) {
const prev = connections.get(id) const prev = connections.get(id)
if (prev?.status === status && prev?.error === error) return if (prev?.status === status && prev?.error === error) return
const next = { workspaceID: id, status, error } const next = { workspaceID: id, status, error }
@@ -364,13 +363,13 @@ export namespace Workspace {
properties: next, properties: next,
}, },
}) })
} }
export function status(): ConnectionStatus[] { export function status(): ConnectionStatus[] {
return [...connections.values()] return [...connections.values()]
} }
function synced(state: Record<string, number>) { function synced(state: Record<string, number>) {
const ids = Object.keys(state) const ids = Object.keys(state)
if (ids.length === 0) return true if (ids.length === 0) return true
@@ -390,13 +389,13 @@ export namespace Workspace {
return ids.every((id) => { return ids.every((id) => {
return (done[id] ?? -1) >= state[id] return (done[id] ?? -1) >= state[id]
}) })
} }
export async function isSyncing(workspaceID: WorkspaceID) { export async function isSyncing(workspaceID: WorkspaceID) {
return aborts.has(workspaceID) return aborts.has(workspaceID)
} }
export async function waitForSync(workspaceID: WorkspaceID, state: Record<string, number>, signal?: AbortSignal) { export async function waitForSync(workspaceID: WorkspaceID, state: Record<string, number>, signal?: AbortSignal) {
if (synced(state)) return if (synced(state)) return
try { try {
@@ -414,19 +413,19 @@ export namespace Workspace {
if (signal?.aborted) throw signal.reason ?? new Error("Request aborted") if (signal?.aborted) throw signal.reason ?? new Error("Request aborted")
throw new Error(`Timed out waiting for sync fence: ${JSON.stringify(state)}`) throw new Error(`Timed out waiting for sync fence: ${JSON.stringify(state)}`)
} }
} }
const log = Log.create({ service: "workspace-sync" }) const log = Log.create({ service: "workspace-sync" })
function route(url: string | URL, path: string) { function route(url: string | URL, path: string) {
const next = new URL(url) const next = new URL(url)
next.pathname = `${next.pathname.replace(/\/$/, "")}${path}` next.pathname = `${next.pathname.replace(/\/$/, "")}${path}`
next.search = "" next.search = ""
next.hash = "" next.hash = ""
return next return next
} }
async function syncWorkspace(space: Info, signal: AbortSignal) { async function syncWorkspace(space: Info, signal: AbortSignal) {
while (!signal.aborted) { while (!signal.aborted) {
log.info("connecting to global sync", { workspace: space.name }) log.info("connecting to global sync", { workspace: space.name })
setStatus(space.id, "connecting") setStatus(space.id, "connecting")
@@ -489,9 +488,9 @@ export namespace Workspace {
// TODO: Implement exponential backoff // TODO: Implement exponential backoff
await sleep(1000) await sleep(1000)
} }
} }
async function startSync(space: Info) { async function startSync(space: Info) {
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
const adaptor = await getAdaptor(space.projectID, space.type) const adaptor = await getAdaptor(space.projectID, space.type)
@@ -520,11 +519,12 @@ export namespace Workspace {
error, error,
}) })
}) })
} }
function stopSync(id: WorkspaceID) { function stopSync(id: WorkspaceID) {
aborts.get(id)?.abort() aborts.get(id)?.abort()
aborts.delete(id) aborts.delete(id)
connections.delete(id) connections.delete(id)
}
} }
export * as Workspace from "./workspace"