Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,12 @@ export const getWorkspace = async (
return response.data
}

export const watchWorkspace = (workspaceId: string): EventSource => {
return new EventSource(`${location.protocol}//${location.host}/api/v2/workspaces/${workspaceId}/watch`,
{ withCredentials: true }
)
}

export const getURLWithSearchParams = (
basePath: string,
filter?: TypesGen.WorkspaceFilter | TypesGen.UsersRequest,
Expand Down
175 changes: 76 additions & 99 deletions site/src/xServices/workspace/workspaceXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const Language = {
type Permissions = Record<keyof ReturnType<typeof permissionsToCheck>, boolean>

export interface WorkspaceContext {
// our server side events instance
sse: EventSource | undefined,
workspace?: TypesGen.Workspace
template?: TypesGen.Template
build?: TypesGen.WorkspaceBuild
Expand All @@ -40,7 +42,7 @@ export interface WorkspaceContext {
// permissions
permissions?: Permissions
checkPermissionsError?: Error | unknown
userId?: string
userId?: string,
}

export type WorkspaceEvent =
Expand All @@ -53,7 +55,10 @@ export type WorkspaceEvent =
| { type: "UPDATE" }
| { type: "CANCEL" }
| { type: "LOAD_MORE_BUILDS" }
| { type: "CHECK_REFRESH_TIMELINE", data: TypesGen.ServerSentEvent["data"] }
| { type: "REFRESH_TIMELINE" }
| { type: "UPDATE_EVENT", data: TypesGen.ServerSentEvent["data"] }
| { type: "SSE_ERROR", error: Error | unknown }

export const checks = {
readWorkspace: "readWorkspace",
Expand Down Expand Up @@ -109,12 +114,9 @@ export const workspaceMachine = createMachine(
cancelWorkspace: {
data: Types.Message
}
refreshWorkspace: {
data: TypesGen.Workspace | undefined
}
getResources: {
data: TypesGen.WorkspaceResource[]
}
listenForEvents: {
data: TypesGen.ServerSentEvent
},
getBuilds: {
data: TypesGen.WorkspaceBuild[]
}
Expand Down Expand Up @@ -196,38 +198,33 @@ export const workspaceMachine = createMachine(
],
},
},
sseFailure: {
entry: ["assignRefreshWorkspaceError", "assignGetResourcesError"],
after: {
"1000": {
target: 'ready.listenForEvents'
}
}
},
ready: {
type: "parallel",
states: {
pollingWorkspace: {
initial: "refreshingWorkspace",
states: {
refreshingWorkspace: {
entry: "clearRefreshWorkspaceError",
invoke: {
src: "refreshWorkspace",
id: "refreshWorkspace",
onDone: [
{
actions: ["refreshTimeline", "assignWorkspace"],
target: "waiting",
},
],
onError: [
{
actions: "assignRefreshWorkspaceError",
target: "waiting",
},
],
},
listenForEvents: {
on: {
UPDATE_EVENT: {
actions: ["updateWorkspace", "assignResources"]
},
waiting: {
after: {
"1000": {
target: "refreshingWorkspace",
},
},
SSE_ERROR: {
target: "#workspaceState.sseFailure"
},
CHECK_REFRESH_TIMELINE: {
actions: ["refreshTimeline"]
}
},
entry: ["clearGetWorkspaceError", "clearGetResourcesError", "initSse"],
exit: "closeSse",
invoke: {
src: "listenForEvents",
},
},
build: {
Expand Down Expand Up @@ -368,37 +365,6 @@ export const workspaceMachine = createMachine(
},
},
},
pollingResources: {
initial: "gettingResources",
states: {
gettingResources: {
entry: "clearGetResourcesError",
invoke: {
src: "getResources",
id: "getResources",
onDone: [
{
actions: "assignResources",
target: "waiting",
},
],
onError: [
{
actions: "assignGetResourcesError",
target: "waiting",
},
],
},
},
waiting: {
after: {
"5000": {
target: "gettingResources",
},
},
},
},
},
timeline: {
initial: "gettingBuilds",
states: {
Expand Down Expand Up @@ -525,11 +491,28 @@ export const workspaceMachine = createMachine(
clearCancellationError: assign({
cancellationError: (_) => undefined,
}),
// SSE related actions
// open a new EventSource so we can stream SSE
initSse: assign({
sse: (context) => context.workspace?.id ? API.watchWorkspace(context.workspace.id) : undefined
}),
closeSse: (context) => context.sse && context.sse.close(),
// updating workspaces
updateWorkspace: assign({
workspace: (_, event) => event.data,
}),
assignRefreshWorkspaceError: assign({
refreshWorkspaceError: (_, event) => event.data,
refreshWorkspaceError: (_, event) => event,
}),
// getting resources
assignResources: assign({
resources: (_, event) => event.data.latest_build.resources,
}),
clearRefreshWorkspaceError: assign({
refreshWorkspaceError: (_) => undefined,
assignGetResourcesError: assign({
getResourcesError: (_, event) => event,
}),
clearGetResourcesError: assign({
getResourcesError: (_) => undefined,
}),
assignRefreshTemplateError: assign({
refreshTemplateError: (_, event) => event.data,
Expand All @@ -540,16 +523,6 @@ export const workspaceMachine = createMachine(
clearRefreshTemplateError: assign({
refreshTemplateError: (_) => undefined,
}),
// Resources
assignResources: assign({
resources: (_, event) => event.data,
}),
assignGetResourcesError: assign({
getResourcesError: (_, event) => event.data,
}),
clearGetResourcesError: assign({
getResourcesError: (_) => undefined,
}),
// Timeline
assignBuilds: assign({
builds: (_, event) => event.data,
Expand Down Expand Up @@ -583,9 +556,10 @@ export const workspaceMachine = createMachine(
if (!context.builds) {
return
}
// When it is a refresh workspace event, we want to check if the latest

// When it is a CHECK_REFRESH_TIMELINE workspace event, we want to check if the latest
// build was updated to not over fetch the builds
if (event.type === "done.invoke.refreshWorkspace") {
if (event.type === "CHECK_REFRESH_TIMELINE") {
const latestBuildInTimeline = latestBuild(context.builds)
const isUpdated = event.data?.latest_build.updated_at !== latestBuildInTimeline.updated_at
if (isUpdated) {
Expand Down Expand Up @@ -650,27 +624,30 @@ export const workspaceMachine = createMachine(
throw Error("Cannot cancel workspace without build id")
}
},
refreshWorkspace: async (context) => {
if (context.workspace) {
return await API.getWorkspaceByOwnerAndName(
context.workspace.owner_name,
context.workspace.name,
{
include_deleted: true,
},
)
} else {
throw Error("Cannot refresh workspace without id")
listenForEvents: (context) => (callback) => {
if (!context.sse) {
callback({ type: "SSE_ERROR", error: "error initializing sse" })
return
}
},
getResources: async (context) => {
// If the job hasn't completed, fetching resources will result
// in an unfriendly error for the user.
if (!context.workspace?.latest_build.job.completed_at) {
return []

context.sse.addEventListener("data", (event) => {
// update our data objects (workspace, resources) with each SSE that comes back from the server
callback({ type: "UPDATE_EVENT", data: JSON.parse(event.data) })
// refresh our timeline
callback({ type: "CHECK_REFRESH_TIMELINE", data: JSON.parse(event.data) })
})

// handle any error events returned by our sse
context.sse.addEventListener("error", (event) => {
callback({ type: "SSE_ERROR", error: event })
})

// handle any sse implementation exceptions
context.sse.onerror = () => {
context.sse && context.sse.close();
callback({ type: "SSE_ERROR", error: "sse error" })
}
const resources = await API.getWorkspaceResources(context.workspace.latest_build.id)
return resources

},
getBuilds: async (context) => {
if (context.workspace) {
Expand Down
2 changes: 2 additions & 0 deletions site/webpack.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const config: Configuration = {
secure: false,
},
},
// We must disable compression to get SSEs to work (in workspaceXService.ts)
compress: false,
static: ["./static"],
},

Expand Down