Files
deepseek-harness/packages/api/workspace-controller/src/feed.ts
T

185 lines
6.3 KiB
TypeScript

/** Reconnect-safe Workspace baseline and increment producer. */
import type { Context } from '@deepseek-ai/cordis'
import type { DomainChanged } from '@deepseek-ai/dsh-storage-domain'
import type { Workspace, WorkspaceRecord } from '@deepseek-ai/dsh-workspace'
import {
workspaceDomainState,
workspaceRecord,
WorkspaceId,
} from '@deepseek-ai/dsh-workspace'
import type {
WorkspaceBaseline,
WorkspaceFollowFrame,
WorkspaceView,
} from './types.ts'
/**
* Project one authoritative Workspace entity into its Remote value.
* @param workspace - authoritative registry entity.
* @returns detached Workspace projection for Remote consumers.
*/
export function workspaceView(workspace: Workspace): WorkspaceView {
return {
workspaceId: workspace.id,
path: workspace.path,
title: workspace.title,
sessionIds: [...workspace.sessionIds],
createdAt: workspace.createdAt,
updatedAt: workspace.updatedAt,
}
}
function changedWorkspaceView(workspaceId: string, value: unknown): WorkspaceView {
const record: WorkspaceRecord = workspaceRecord.parse(value)
return {
workspaceId: WorkspaceId(workspaceId),
path: record.path,
title: record.title,
sessionIds: [...record.sessionIds],
createdAt: record.createdAt,
updatedAt: record.updatedAt,
}
}
/** Owns Workspace domain observation and all active follow generations. */
export class WorkspaceFeed {
private readonly followers = new Set<WorkspaceFollower>()
private knownIds: Set<string>
private order: readonly string[]
private archived: readonly string[]
/** @param ctx - Host context containing the authoritative Workspace registry. */
constructor(private readonly ctx: Context) {
const baseline = ctx.workspaceRegistry.list()
this.knownIds = new Set(baseline.map(workspace => String(workspace.id)))
this.order = baseline.map(workspace => String(workspace.id))
this.archived = ctx.workspaceRegistry.archivedSessionIds.map(String)
ctx.on('domain/changed', (change: DomainChanged) => { this.changed(change) })
ctx.effect(() => () => {
for (const follower of this.followers) follower.close()
this.followers.clear()
}, 'workspace-controller.feed')
}
/**
* Read the complete current projection synchronously.
* @returns all active Workspaces and archived Session identities.
*/
baseline(): WorkspaceBaseline {
return {
items: this.ctx.workspaceRegistry.list().map(workspaceView),
archivedSessionIds: [...this.ctx.workspaceRegistry.archivedSessionIds],
}
}
/**
* Open one generation beginning with a complete baseline.
* @param signal - generation cancellation.
* @returns baseline followed by ordered Workspace increments.
*/
async *follow(signal: AbortSignal): AsyncIterable<WorkspaceFollowFrame> {
signal.throwIfAborted()
const follower = new WorkspaceFollower()
this.followers.add(follower)
try {
yield { type: 'baseline', value: this.baseline() }
yield* follower.read(signal)
} finally {
this.followers.delete(follower)
follower.close()
}
}
private changed(change: DomainChanged): void {
if (change.domain !== 'workspace') return
if (change.table === '') {
if (change.operation !== 'put') return
const state = workspaceDomainState.parse(change.value)
const nextOrder = state.workspaceIds.map(String)
const orderChanged = !sameStrings(this.order, nextOrder)
for (const id of state.workspaceIds) {
if (this.knownIds.has(id)) continue
const workspace = this.ctx.workspaceRegistry.get(id)
if (workspace === undefined) {
throw new Error(`committed Workspace registry references missing Workspace "${id}"`)
}
this.knownIds.add(id)
this.publish({ type: 'upsert', workspace: workspaceView(workspace) })
}
this.order = nextOrder
if (orderChanged) this.publish({ type: 'order', workspaceIds: [...state.workspaceIds] })
const nextArchived = state.archivedSessionIds.map(String)
if (!sameStrings(this.archived, nextArchived)) {
this.archived = nextArchived
this.publish({ type: 'archived', archivedSessionIds: [...state.archivedSessionIds] })
}
return
}
if (change.table !== 'workspaces') return
if (change.operation === 'deleted') {
if (!this.knownIds.delete(change.key)) return
this.publish({ type: 'remove', workspaceId: WorkspaceId(change.key) })
return
}
if (!this.knownIds.has(change.key)) return
this.publish({
type: 'upsert',
workspace: changedWorkspaceView(change.key, change.value),
})
}
private publish(frame: Exclude<WorkspaceFollowFrame, { readonly type: 'baseline' }>): void {
for (const follower of this.followers) follower.push(frame)
}
}
function sameStrings(left: readonly string[], right: readonly string[]): boolean {
return left.length === right.length && left.every((value, index) => value === right[index])
}
class WorkspaceFollower {
private readonly frames: WorkspaceFollowFrame[] = []
private waiting: (() => void) | undefined
private closed = false
push(frame: WorkspaceFollowFrame): void {
/* v8 ignore next -- closed followers are removed before later publication can reach them. */
if (this.closed) return
this.frames.push(frame)
this.waiting?.()
}
close(): void {
if (this.closed) return
this.closed = true
this.waiting?.()
}
async *read(signal: AbortSignal): AsyncIterable<WorkspaceFollowFrame> {
while (!this.closed && !signal.aborted) {
const frame = this.frames.shift()
if (frame !== undefined) {
yield frame
continue
}
await this.wait(signal)
}
}
private wait(signal: AbortSignal): Promise<void> {
return new Promise((resolve) => {
const finish = (): void => {
signal.removeEventListener('abort', finish)
/* v8 ignore next -- one read owns the sole installed wait callback. */
if (this.waiting === finish) this.waiting = undefined
resolve()
}
this.waiting = finish
signal.addEventListener('abort', finish, { once: true })
/* v8 ignore next -- native signals and the private queue cannot change during this synchronous setup. */
if (signal.aborted || this.closed || this.frames.length > 0) finish()
})
}
}