commands.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /** Workspace command implementation and stable Remote failure mapping. */
  2. import type { Context } from '@deepseek-ai/cordis'
  3. import type { Workspace } from '@deepseek-ai/dsh-workspace'
  4. import {
  5. WorkspaceId,
  6. WorkspaceMoveInvalidError,
  7. WorkspaceOrderInvalidError,
  8. WorkspaceUnknownSessionError,
  9. } from '@deepseek-ai/dsh-workspace'
  10. import { TypertRemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
  11. import { workspaceView } from './feed.ts'
  12. import type {
  13. WorkspaceArchiveSessionRequest,
  14. WorkspaceArchiveValue,
  15. WorkspaceCreateRequest,
  16. WorkspaceCreateValue,
  17. WorkspaceDeleteRequest,
  18. WorkspaceDeleteValue,
  19. WorkspaceInsertBeforeRequest,
  20. WorkspaceInsertSessionBeforeRequest,
  21. WorkspaceOrderValue,
  22. WorkspaceRenameRequest,
  23. WorkspaceValue,
  24. } from './types.ts'
  25. /** Implements Workspace mutations against the authoritative registry. */
  26. export class WorkspaceCommands {
  27. private operationTail = Promise.resolve()
  28. /** @param ctx - Host context containing the Workspace registry. */
  29. constructor(private readonly ctx: Context) {}
  30. /**
  31. * Create or resolve one Workspace over an existing directory.
  32. * @param request - directory path to register.
  33. * @returns the Workspace and whether this call created it.
  34. */
  35. create(request: WorkspaceCreateRequest): Promise<WorkspaceCreateValue> {
  36. return this.enqueue(async () => {
  37. try {
  38. const existing = await this.ctx.workspaceRegistry.resolveByPath(request.path)
  39. if (existing !== undefined) {
  40. return { workspace: workspaceView(existing), created: false }
  41. }
  42. const workspace = await this.ctx.workspaceRegistry.create(request.path)
  43. return { workspace: workspaceView(workspace), created: true }
  44. } catch (error) {
  45. if (error instanceof TypertRemoteFailure) throw error
  46. throw failure(
  47. 'workspace-invalid-path',
  48. `cannot create a Workspace at "${request.path}": ${errorMessage(error)}`,
  49. { path: request.path },
  50. )
  51. }
  52. })
  53. }
  54. /**
  55. * Rename one Workspace after serializing title ownership checks.
  56. * @param request - Workspace identity and proposed title.
  57. * @returns the updated Workspace projection.
  58. */
  59. rename(request: WorkspaceRenameRequest): Promise<WorkspaceValue> {
  60. const title = request.title.trim()
  61. if (title === '') {
  62. return Promise.reject(failure(
  63. 'bad-request',
  64. 'Workspace rename requires a non-blank title',
  65. {},
  66. ))
  67. }
  68. return this.enqueue(async () => {
  69. const workspace = this.requireWorkspace(request.workspaceId)
  70. if (title !== workspace.title) {
  71. if (this.ctx.workspaceRegistry.list().some(candidate =>
  72. candidate.id !== workspace.id && candidate.title === title)) {
  73. throw failure(
  74. 'workspace-name-conflict',
  75. `Workspace name '${title}' is already in use`,
  76. { name: title },
  77. )
  78. }
  79. await workspace.setTitle(title)
  80. }
  81. return { workspace: workspaceView(workspace) }
  82. })
  83. }
  84. /**
  85. * Delete one Workspace registration without deleting its directory or Sessions.
  86. * @param request - Workspace identity to remove.
  87. * @returns deletion confirmation.
  88. */
  89. delete(request: WorkspaceDeleteRequest): Promise<WorkspaceDeleteValue> {
  90. return this.enqueue(async () => {
  91. if (!await this.ctx.workspaceRegistry.delete(WorkspaceId(request.workspaceId))) {
  92. throw workspaceNotFound(request.workspaceId)
  93. }
  94. return { deleted: true }
  95. })
  96. }
  97. /**
  98. * Move one Workspace within the durable registry order.
  99. * @param request - moved Workspace and optional anchor.
  100. * @returns the complete resulting Workspace order.
  101. */
  102. async insertBefore(request: WorkspaceInsertBeforeRequest): Promise<WorkspaceOrderValue> {
  103. try {
  104. const workspaceIds = await this.ctx.workspaceRegistry.insertBefore(
  105. WorkspaceId(request.workspaceId),
  106. request.beforeWorkspaceId === undefined
  107. ? undefined
  108. : WorkspaceId(request.beforeWorkspaceId),
  109. )
  110. return { workspaceIds: [...workspaceIds] }
  111. } catch (error) {
  112. if (!(error instanceof WorkspaceOrderInvalidError)) throw error
  113. throw workspaceNotFound(error.workspaceId)
  114. }
  115. }
  116. /**
  117. * Move one accounted Session within a Workspace's manual order.
  118. * @param request - Workspace, Session, and optional anchor identities.
  119. * @returns the updated Workspace projection.
  120. */
  121. async insertSessionBefore(request: WorkspaceInsertSessionBeforeRequest): Promise<WorkspaceValue> {
  122. const workspace = this.requireWorkspace(request.workspaceId)
  123. try {
  124. await workspace.insertSessionBefore(request.sessionId, request.beforeSessionId)
  125. } catch (error) {
  126. if (!(error instanceof WorkspaceMoveInvalidError)) throw error
  127. throw failure(
  128. 'workspace-move-invalid',
  129. error.message,
  130. {
  131. workspaceId: request.workspaceId,
  132. sessionId: request.sessionId,
  133. ...request.beforeSessionId === undefined
  134. ? {}
  135. : { beforeSessionId: request.beforeSessionId },
  136. },
  137. )
  138. }
  139. return { workspace: workspaceView(workspace) }
  140. }
  141. /**
  142. * Add one known Session to the registry-global archive set.
  143. * @param request - Session identity to archive.
  144. * @returns the complete resulting archive set.
  145. */
  146. async archiveSession(request: WorkspaceArchiveSessionRequest): Promise<WorkspaceArchiveValue> {
  147. try {
  148. await this.ctx.workspaceRegistry.archiveSession(request.sessionId)
  149. } catch (error) {
  150. if (!(error instanceof WorkspaceUnknownSessionError)) throw error
  151. throw failure('session-not-found', error.message, { sessionId: request.sessionId })
  152. }
  153. return { archivedSessionIds: [...this.ctx.workspaceRegistry.archivedSessionIds] }
  154. }
  155. private requireWorkspace(workspaceId: WorkspaceId): Workspace {
  156. const workspace = this.ctx.workspaceRegistry.get(WorkspaceId(workspaceId))
  157. if (workspace === undefined) throw workspaceNotFound(workspaceId)
  158. return workspace
  159. }
  160. private enqueue<T>(operation: () => Promise<T>): Promise<T> {
  161. const result = this.operationTail.then(operation)
  162. this.operationTail = result.then(() => undefined, () => undefined)
  163. return result
  164. }
  165. }
  166. function workspaceNotFound(workspaceId: WorkspaceId): TypertRemoteFailure {
  167. return failure(
  168. 'workspace-not-found',
  169. `Workspace "${workspaceId}" not found`,
  170. { workspaceId },
  171. )
  172. }
  173. function failure(
  174. code: string,
  175. message: string,
  176. details: object,
  177. ): TypertRemoteFailure {
  178. return new TypertRemoteFailure({ code, message, details })
  179. }
  180. function errorMessage(error: unknown): string {
  181. return error instanceof Error ? error.message : String(error)
  182. }