refactor: unwrap Workspace namespace + self-reexport (#22934)
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user