mirror of
https://github.com/enso-org/enso.git
synced 2024-08-17 00:40:34 +03:00
Restore state after reconnecting (#9755)
Fixes: #8522 Execution context is refactored slightly: now we have a single `sync` function to synchronize both visualization and execution stack. Tested hibernation on Linux: I was able to continue my work 🎉 # Important Notes The Refinement Notes state, that the execution mode should be set before updating the stack, but actually it makes an error on startup (changing context automatically re-executes programs, what fails if there's no frame on the stack).
This commit is contained in:
parent
931baa4276
commit
5807c5c112
@ -496,6 +496,7 @@ export const mockLSHandler: MockTransportData = async (method, data, transport)
|
||||
case 'executionContext/push':
|
||||
case 'executionContext/pop':
|
||||
case 'executionContext/recompute':
|
||||
case 'executionContext/setExecutionEnvironment':
|
||||
case 'capability/acquire':
|
||||
return {}
|
||||
case 'file/list': {
|
||||
|
@ -267,7 +267,7 @@ function onRecordOnceButtonPress() {
|
||||
watch(
|
||||
() => projectStore.executionMode,
|
||||
(modeValue) => {
|
||||
projectStore.executionContext.setExecutionEnvironment(modeValue === 'live' ? 'Live' : 'Design')
|
||||
projectStore.executionContext.executionEnvironment = modeValue === 'live' ? 'Live' : 'Design'
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -43,23 +43,7 @@ export function useStackNavigator() {
|
||||
}
|
||||
|
||||
function handleBreadcrumbClick(index: number) {
|
||||
const activeStack = projectStore.executionContext.desiredStack
|
||||
// Number of items in desired stack should be index + 1
|
||||
if (index + 1 < activeStack.length) {
|
||||
for (let i = activeStack.length; i > index + 1; i--) {
|
||||
projectStore.executionContext.pop()
|
||||
}
|
||||
} else if (index + 1 > activeStack.length) {
|
||||
for (let i = activeStack.length; i <= index; i++) {
|
||||
const stackItem = breadcrumbs.value[i]
|
||||
if (stackItem?.type === 'LocalCall') {
|
||||
const exprId = stackItem.expressionId
|
||||
projectStore.executionContext.push(exprId)
|
||||
} else {
|
||||
console.warn('Cannot enter non-local call.')
|
||||
}
|
||||
}
|
||||
}
|
||||
projectStore.executionContext.desiredStack = breadcrumbs.value.slice(0, index + 1)
|
||||
graphStore.updateState()
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { findIndexOpt } from '@/util/data/array'
|
||||
import { isSome, type Opt } from '@/util/data/opt'
|
||||
import { Err, Ok, type Result } from '@/util/data/result'
|
||||
import { AsyncQueue, type AbortScope } from '@/util/net'
|
||||
@ -6,16 +7,17 @@ import * as object from 'lib0/object'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
import * as random from 'lib0/random'
|
||||
import type { LanguageServer } from 'shared/languageServer'
|
||||
import type {
|
||||
ContextId,
|
||||
Diagnostic,
|
||||
ExecutionEnvironment,
|
||||
ExplicitCall,
|
||||
ExpressionId,
|
||||
ExpressionUpdate,
|
||||
StackItem,
|
||||
Uuid,
|
||||
VisualizationConfiguration,
|
||||
import {
|
||||
stackItemsEqual,
|
||||
type ContextId,
|
||||
type Diagnostic,
|
||||
type ExecutionEnvironment,
|
||||
type ExplicitCall,
|
||||
type ExpressionId,
|
||||
type ExpressionUpdate,
|
||||
type StackItem,
|
||||
type Uuid,
|
||||
type VisualizationConfiguration,
|
||||
} from 'shared/languageServerTypes'
|
||||
import { exponentialBackoff } from 'shared/util/net'
|
||||
import type { ExternalId } from 'shared/yjsModel'
|
||||
@ -46,12 +48,14 @@ function visualizationConfigEqual(
|
||||
)
|
||||
}
|
||||
|
||||
interface ExecutionContextState {
|
||||
lsRpc: LanguageServer
|
||||
created: boolean
|
||||
visualizations: Map<Uuid, NodeVisualizationConfiguration>
|
||||
stack: StackItem[]
|
||||
}
|
||||
type ExecutionContextState =
|
||||
| { status: 'not-created' }
|
||||
| {
|
||||
status: 'created'
|
||||
visualizations: Map<Uuid, NodeVisualizationConfiguration>
|
||||
stack: StackItem[]
|
||||
environment?: ExecutionEnvironment
|
||||
} // | { status: 'broken'} TODO[ao] think about it
|
||||
|
||||
type EntryPoint = Omit<ExplicitCall, 'type'>
|
||||
|
||||
@ -80,146 +84,79 @@ type ExecutionContextNotification = {
|
||||
* run only when the previous call is done.
|
||||
*/
|
||||
export class ExecutionContext extends ObservableV2<ExecutionContextNotification> {
|
||||
id: ContextId = random.uuidv4() as ContextId
|
||||
queue: AsyncQueue<ExecutionContextState>
|
||||
taskRunning = false
|
||||
visSyncScheduled = false
|
||||
desiredStack: StackItem[] = reactive([])
|
||||
visualizationConfigs: Map<Uuid, NodeVisualizationConfiguration> = new Map()
|
||||
readonly id: ContextId = random.uuidv4() as ContextId
|
||||
private queue: AsyncQueue<ExecutionContextState>
|
||||
private syncScheduled = false
|
||||
private clearScheduled = false
|
||||
private _desiredStack: StackItem[] = reactive([])
|
||||
private visualizationConfigs: Map<Uuid, NodeVisualizationConfiguration> = new Map()
|
||||
private _executionEnvironment: ExecutionEnvironment = 'Design'
|
||||
|
||||
constructor(
|
||||
lsRpc: LanguageServer,
|
||||
private lsRpc: LanguageServer,
|
||||
entryPoint: EntryPoint,
|
||||
private abort: AbortScope,
|
||||
) {
|
||||
super()
|
||||
this.abort.handleDispose(this)
|
||||
|
||||
this.queue = new AsyncQueue<ExecutionContextState>(
|
||||
Promise.resolve({
|
||||
lsRpc,
|
||||
created: false,
|
||||
visualizations: new Map(),
|
||||
stack: [],
|
||||
}),
|
||||
)
|
||||
this.lsRpc.retain()
|
||||
this.queue = new AsyncQueue<ExecutionContextState>(Promise.resolve({ status: 'not-created' }))
|
||||
this.registerHandlers()
|
||||
this.create()
|
||||
this.pushItem({ type: 'ExplicitCall', ...entryPoint })
|
||||
this.recompute()
|
||||
}
|
||||
|
||||
private async withBackoff<T>(f: () => Promise<Result<T>>, message: string): Promise<T> {
|
||||
const result = await exponentialBackoff(f, {
|
||||
onBeforeRetry: (error, _, delay) => {
|
||||
if (this.abort.signal.aborted) return false
|
||||
console.warn(`${error.message(message)}. Retrying after ${delay}ms...\n`)
|
||||
},
|
||||
private registerHandlers() {
|
||||
this.abort.handleObserve(this.lsRpc, 'executionContext/expressionUpdates', (event) => {
|
||||
if (event.contextId == this.id) this.emit('expressionUpdates', [event.updates])
|
||||
})
|
||||
if (result.ok) return result.value
|
||||
else throw result.error
|
||||
}
|
||||
|
||||
private syncVisualizations() {
|
||||
if (this.visSyncScheduled || this.abort.signal.aborted) return
|
||||
this.visSyncScheduled = true
|
||||
this.queue.pushTask(async (state) => {
|
||||
this.visSyncScheduled = false
|
||||
if (!state.created || this.abort.signal.aborted) return state
|
||||
this.emit('newVisualizationConfiguration', [new Set(this.visualizationConfigs.keys())])
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
const attach = (id: Uuid, config: NodeVisualizationConfiguration) => {
|
||||
return this.withBackoff(
|
||||
() =>
|
||||
state.lsRpc.attachVisualization(id, config.expressionId, {
|
||||
executionContextId: this.id,
|
||||
expression: config.expression,
|
||||
visualizationModule: config.visualizationModule,
|
||||
...(config.positionalArgumentsExpressions ?
|
||||
{ positionalArgumentsExpressions: config.positionalArgumentsExpressions }
|
||||
: {}),
|
||||
}),
|
||||
'Failed to attach visualization',
|
||||
).then(() => {
|
||||
state.visualizations.set(id, config)
|
||||
})
|
||||
}
|
||||
|
||||
const modify = (id: Uuid, config: NodeVisualizationConfiguration) => {
|
||||
return this.withBackoff(
|
||||
() =>
|
||||
state.lsRpc.modifyVisualization(id, {
|
||||
executionContextId: this.id,
|
||||
expression: config.expression,
|
||||
visualizationModule: config.visualizationModule,
|
||||
...(config.positionalArgumentsExpressions ?
|
||||
{ positionalArgumentsExpressions: config.positionalArgumentsExpressions }
|
||||
: {}),
|
||||
}),
|
||||
'Failed to modify visualization',
|
||||
).then(() => {
|
||||
state.visualizations.set(id, config)
|
||||
})
|
||||
}
|
||||
|
||||
const detach = (id: Uuid, config: NodeVisualizationConfiguration) => {
|
||||
return this.withBackoff(
|
||||
() => state.lsRpc.detachVisualization(id, config.expressionId, this.id),
|
||||
'Failed to detach visualization',
|
||||
).then(() => {
|
||||
state.visualizations.delete(id)
|
||||
})
|
||||
}
|
||||
|
||||
// Attach new and update existing visualizations.
|
||||
for (const [id, config] of this.visualizationConfigs) {
|
||||
const previousConfig = state.visualizations.get(id)
|
||||
if (previousConfig == null) {
|
||||
promises.push(attach(id, config))
|
||||
} else if (!visualizationConfigEqual(previousConfig, config)) {
|
||||
if (previousConfig.expressionId === config.expressionId) {
|
||||
promises.push(modify(id, config))
|
||||
} else {
|
||||
promises.push(detach(id, previousConfig).then(() => attach(id, config)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detach removed visualizations.
|
||||
for (const [id, config] of state.visualizations) {
|
||||
if (!this.visualizationConfigs.get(id)) {
|
||||
promises.push(detach(id, config))
|
||||
}
|
||||
}
|
||||
const settled = await Promise.allSettled(promises)
|
||||
|
||||
// Emit errors for failed requests.
|
||||
const errors = settled
|
||||
.map((result) => (result.status === 'rejected' ? result.reason : null))
|
||||
.filter(isSome)
|
||||
if (errors.length > 0) {
|
||||
console.error('Failed to synchronize visualizations:', errors)
|
||||
}
|
||||
|
||||
this.emit('visualizationsConfigured', [new Set(this.visualizationConfigs.keys())])
|
||||
|
||||
// State object was updated in-place in each successful promise.
|
||||
return state
|
||||
this.abort.handleObserve(this.lsRpc, 'executionContext/executionFailed', (event) => {
|
||||
if (event.contextId == this.id) this.emit('executionFailed', [event.message])
|
||||
})
|
||||
this.abort.handleObserve(this.lsRpc, 'executionContext/executionComplete', (event) => {
|
||||
if (event.contextId == this.id) this.emit('executionComplete', [])
|
||||
})
|
||||
this.abort.handleObserve(this.lsRpc, 'executionContext/executionStatus', (event) => {
|
||||
if (event.contextId == this.id) this.emit('executionStatus', [event.diagnostics])
|
||||
})
|
||||
this.abort.handleObserve(
|
||||
this.lsRpc,
|
||||
'executionContext/visualizationEvaluationFailed',
|
||||
(event) => {
|
||||
if (event.contextId == this.id)
|
||||
this.emit('visualizationEvaluationFailed', [
|
||||
event.visualizationId,
|
||||
event.expressionId,
|
||||
event.message,
|
||||
event.diagnostic,
|
||||
])
|
||||
},
|
||||
)
|
||||
this.lsRpc.on('transport/closed', () => {
|
||||
// Connection closed: the created execution context is no longer available
|
||||
// There is no point in any scheduled action until resynchronization
|
||||
this.queue.clear()
|
||||
this.syncScheduled = false
|
||||
this.queue.pushTask(() => {
|
||||
this.clearScheduled = false
|
||||
this.sync()
|
||||
return Promise.resolve({ status: 'not-created' })
|
||||
})
|
||||
this.clearScheduled = true
|
||||
})
|
||||
}
|
||||
|
||||
private pushItem(item: StackItem) {
|
||||
this.desiredStack.push(item)
|
||||
this.queue.pushTask(async (state) => {
|
||||
if (!state.created) return state
|
||||
await this.withBackoff(
|
||||
() => state.lsRpc.pushExecutionContextItem(this.id, item),
|
||||
'Failed to push item to execution context stack',
|
||||
)
|
||||
state.stack.push(item)
|
||||
return state
|
||||
})
|
||||
this._desiredStack.push(item)
|
||||
this.sync()
|
||||
}
|
||||
|
||||
get desiredStack() {
|
||||
return this._desiredStack
|
||||
}
|
||||
|
||||
set desiredStack(stack: StackItem[]) {
|
||||
this._desiredStack = stack
|
||||
this.sync()
|
||||
}
|
||||
|
||||
push(expressionId: ExpressionId) {
|
||||
@ -227,79 +164,21 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
||||
}
|
||||
|
||||
pop() {
|
||||
if (this.desiredStack.length === 1) {
|
||||
if (this._desiredStack.length === 1) {
|
||||
console.debug('Cannot pop last item from execution context stack')
|
||||
return
|
||||
}
|
||||
this.desiredStack.pop()
|
||||
this.queue.pushTask(async (state) => {
|
||||
if (!state.created) return state
|
||||
if (state.stack.length === 1) {
|
||||
console.debug('Cannot pop last item from execution context stack')
|
||||
return state
|
||||
}
|
||||
await this.withBackoff(
|
||||
() => state.lsRpc.popExecutionContextItem(this.id),
|
||||
'Failed to pop item from execution context stack',
|
||||
)
|
||||
state.stack.pop()
|
||||
return state
|
||||
})
|
||||
this._desiredStack.pop()
|
||||
this.sync()
|
||||
}
|
||||
|
||||
async setVisualization(id: Uuid, configuration: Opt<NodeVisualizationConfiguration>) {
|
||||
setVisualization(id: Uuid, configuration: Opt<NodeVisualizationConfiguration>) {
|
||||
if (configuration == null) {
|
||||
this.visualizationConfigs.delete(id)
|
||||
} else {
|
||||
this.visualizationConfigs.set(id, configuration)
|
||||
}
|
||||
this.syncVisualizations()
|
||||
}
|
||||
|
||||
private create() {
|
||||
this.queue.pushTask(async (state) => {
|
||||
if (state.created) return state
|
||||
return this.withBackoff(async () => {
|
||||
const result = await state.lsRpc.createExecutionContext(this.id)
|
||||
if (!result.ok) return result
|
||||
if (result.value.contextId !== this.id) {
|
||||
return Err('Unexpected Context ID returned by the language server.')
|
||||
}
|
||||
state.lsRpc.retain()
|
||||
return Ok({ ...state, created: true })
|
||||
}, 'Failed to create execution context')
|
||||
})
|
||||
}
|
||||
|
||||
private registerHandlers() {
|
||||
this.queue.pushTask(async (state) => {
|
||||
this.abort.handleObserve(state.lsRpc, 'executionContext/expressionUpdates', (event) => {
|
||||
if (event.contextId == this.id) this.emit('expressionUpdates', [event.updates])
|
||||
})
|
||||
this.abort.handleObserve(state.lsRpc, 'executionContext/executionFailed', (event) => {
|
||||
if (event.contextId == this.id) this.emit('executionFailed', [event.message])
|
||||
})
|
||||
this.abort.handleObserve(state.lsRpc, 'executionContext/executionComplete', (event) => {
|
||||
if (event.contextId == this.id) this.emit('executionComplete', [])
|
||||
})
|
||||
this.abort.handleObserve(state.lsRpc, 'executionContext/executionStatus', (event) => {
|
||||
if (event.contextId == this.id) this.emit('executionStatus', [event.diagnostics])
|
||||
})
|
||||
this.abort.handleObserve(
|
||||
state.lsRpc,
|
||||
'executionContext/visualizationEvaluationFailed',
|
||||
(event) => {
|
||||
if (event.contextId == this.id)
|
||||
this.emit('visualizationEvaluationFailed', [
|
||||
event.visualizationId,
|
||||
event.expressionId,
|
||||
event.message,
|
||||
event.diagnostic,
|
||||
])
|
||||
},
|
||||
)
|
||||
return state
|
||||
})
|
||||
this.sync()
|
||||
}
|
||||
|
||||
recompute(
|
||||
@ -307,36 +186,220 @@ export class ExecutionContext extends ObservableV2<ExecutionContextNotification>
|
||||
executionEnvironment?: ExecutionEnvironment,
|
||||
) {
|
||||
this.queue.pushTask(async (state) => {
|
||||
if (!state.created) return state
|
||||
await state.lsRpc.recomputeExecutionContext(this.id, expressionIds, executionEnvironment)
|
||||
if (state.status !== 'created') {
|
||||
this.sync()
|
||||
return state
|
||||
}
|
||||
await this.lsRpc.recomputeExecutionContext(this.id, expressionIds, executionEnvironment)
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
getStackBottom(): StackItem {
|
||||
return this.desiredStack[0]!
|
||||
return this._desiredStack[0]!
|
||||
}
|
||||
|
||||
getStackTop(): StackItem {
|
||||
return this.desiredStack[this.desiredStack.length - 1]!
|
||||
return this._desiredStack[this._desiredStack.length - 1]!
|
||||
}
|
||||
|
||||
setExecutionEnvironment(mode: ExecutionEnvironment) {
|
||||
this.queue.pushTask(async (state) => {
|
||||
await state.lsRpc.setExecutionEnvironment(this.id, mode)
|
||||
return state
|
||||
})
|
||||
get executionEnvironment() {
|
||||
return this._executionEnvironment
|
||||
}
|
||||
|
||||
set executionEnvironment(env: ExecutionEnvironment) {
|
||||
this._executionEnvironment = env
|
||||
this.sync()
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.queue.pushTask(async (state) => {
|
||||
if (!state.created) return state
|
||||
const result = await state.lsRpc.destroyExecutionContext(this.id)
|
||||
if (!result.ok) {
|
||||
result.error.log('Failed to destroy execution context')
|
||||
if (state.status === 'created') {
|
||||
const result = await this.withBackoff(
|
||||
() => this.lsRpc.destroyExecutionContext(this.id),
|
||||
'Destroying execution context',
|
||||
)
|
||||
if (!result.ok) {
|
||||
result.error.log('Failed to destroy execution context')
|
||||
}
|
||||
}
|
||||
state.lsRpc.release()
|
||||
return { ...state, created: false }
|
||||
this.lsRpc.release()
|
||||
return { status: 'not-created' }
|
||||
})
|
||||
}
|
||||
|
||||
private sync() {
|
||||
if (this.syncScheduled || this.abort.signal.aborted) return
|
||||
this.syncScheduled = true
|
||||
this.queue.pushTask(this.syncTask())
|
||||
}
|
||||
|
||||
private withBackoff<T>(f: () => Promise<Result<T>>, message: string): Promise<Result<T>> {
|
||||
return exponentialBackoff(f, {
|
||||
onBeforeRetry: (error, _, delay) => {
|
||||
if (this.abort.signal.aborted || this.clearScheduled) return false
|
||||
console.warn(`${error.message(message)}. Retrying after ${delay}ms...\n`)
|
||||
},
|
||||
onFailure(error) {
|
||||
error.log(message)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private syncTask() {
|
||||
return async (state: ExecutionContextState) => {
|
||||
this.syncScheduled = false
|
||||
if (this.abort.signal.aborted) return state
|
||||
let newState = { ...state }
|
||||
|
||||
const create = () => {
|
||||
if (newState.status === 'created') return Ok()
|
||||
// if (newState.status === 'broken') {
|
||||
// this.withBackoff(() => this.lsRpc.destroyExecutionContext(this.id), 'Failed to destroy broken execution context')
|
||||
// }
|
||||
return this.withBackoff(async () => {
|
||||
const result = await this.lsRpc.createExecutionContext(this.id)
|
||||
if (!result.ok) return result
|
||||
if (result.value.contextId !== this.id) {
|
||||
return Err('Unexpected Context ID returned by the language server.')
|
||||
}
|
||||
newState = { status: 'created', visualizations: new Map(), stack: [] }
|
||||
return Ok()
|
||||
}, 'Failed to create execution context')
|
||||
}
|
||||
|
||||
const syncEnvironment = async () => {
|
||||
const state = newState
|
||||
if (state.status !== 'created')
|
||||
return Err('Cannot sync execution environment when context is not created')
|
||||
if (state.environment === this._executionEnvironment) return Ok()
|
||||
const result = await this.lsRpc.setExecutionEnvironment(this.id, this._executionEnvironment)
|
||||
if (!result.ok) return result
|
||||
state.environment = this._executionEnvironment
|
||||
return Ok()
|
||||
}
|
||||
|
||||
const syncStack = async () => {
|
||||
const state = newState
|
||||
if (state.status !== 'created')
|
||||
return Err('Cannot sync stack when execution context is not created')
|
||||
const firstDifferent =
|
||||
findIndexOpt(this._desiredStack, (item, index) => {
|
||||
const stateStack = state.stack[index]
|
||||
return stateStack == null || !stackItemsEqual(item, stateStack)
|
||||
}) ?? this._desiredStack.length
|
||||
for (let i = state.stack.length; i > firstDifferent; --i) {
|
||||
const popResult = await this.withBackoff(
|
||||
() => this.lsRpc.popExecutionContextItem(this.id),
|
||||
'Failed to pop execution stack frame',
|
||||
)
|
||||
if (popResult.ok) state.stack.pop()
|
||||
else return popResult
|
||||
}
|
||||
for (let i = state.stack.length; i < this._desiredStack.length; ++i) {
|
||||
const newItem = this._desiredStack[i]!
|
||||
const pushResult = await this.withBackoff(
|
||||
() => this.lsRpc.pushExecutionContextItem(this.id, newItem),
|
||||
'Failed to push execution stack frame',
|
||||
)
|
||||
if (pushResult.ok) state.stack.push(newItem)
|
||||
else return pushResult
|
||||
}
|
||||
return Ok()
|
||||
}
|
||||
|
||||
const syncVisualizations = async () => {
|
||||
const state = newState
|
||||
if (state.status !== 'created')
|
||||
return Err('Cannot sync visualizations when execution context is not created')
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
const attach = (id: Uuid, config: NodeVisualizationConfiguration) => {
|
||||
return this.withBackoff(
|
||||
() =>
|
||||
this.lsRpc.attachVisualization(id, config.expressionId, {
|
||||
executionContextId: this.id,
|
||||
expression: config.expression,
|
||||
visualizationModule: config.visualizationModule,
|
||||
...(config.positionalArgumentsExpressions ?
|
||||
{ positionalArgumentsExpressions: config.positionalArgumentsExpressions }
|
||||
: {}),
|
||||
}),
|
||||
'Failed to attach visualization',
|
||||
).then((result) => {
|
||||
if (result.ok) state.visualizations.set(id, config)
|
||||
})
|
||||
}
|
||||
|
||||
const modify = (id: Uuid, config: NodeVisualizationConfiguration) => {
|
||||
return this.withBackoff(
|
||||
() =>
|
||||
this.lsRpc.modifyVisualization(id, {
|
||||
executionContextId: this.id,
|
||||
expression: config.expression,
|
||||
visualizationModule: config.visualizationModule,
|
||||
...(config.positionalArgumentsExpressions ?
|
||||
{ positionalArgumentsExpressions: config.positionalArgumentsExpressions }
|
||||
: {}),
|
||||
}),
|
||||
'Failed to modify visualization',
|
||||
).then((result) => {
|
||||
if (result.ok) state.visualizations.set(id, config)
|
||||
})
|
||||
}
|
||||
|
||||
const detach = (id: Uuid, config: NodeVisualizationConfiguration) => {
|
||||
return this.withBackoff(
|
||||
() => this.lsRpc.detachVisualization(id, config.expressionId, this.id),
|
||||
'Failed to detach visualization',
|
||||
).then((result) => {
|
||||
if (result.ok) state.visualizations.delete(id)
|
||||
})
|
||||
}
|
||||
|
||||
// Attach new and update existing visualizations.
|
||||
for (const [id, config] of this.visualizationConfigs) {
|
||||
const previousConfig = state.visualizations.get(id)
|
||||
if (previousConfig == null) {
|
||||
promises.push(attach(id, config))
|
||||
} else if (!visualizationConfigEqual(previousConfig, config)) {
|
||||
if (previousConfig.expressionId === config.expressionId) {
|
||||
promises.push(modify(id, config))
|
||||
} else {
|
||||
promises.push(detach(id, previousConfig).then(() => attach(id, config)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detach removed visualizations.
|
||||
for (const [id, config] of state.visualizations) {
|
||||
if (!this.visualizationConfigs.get(id)) {
|
||||
promises.push(detach(id, config))
|
||||
}
|
||||
}
|
||||
const settled = await Promise.allSettled(promises)
|
||||
|
||||
// Emit errors for failed requests.
|
||||
const errors = settled
|
||||
.map((result) => (result.status === 'rejected' ? result.reason : null))
|
||||
.filter(isSome)
|
||||
if (errors.length > 0) {
|
||||
console.error('Failed to synchronize visualizations:', errors)
|
||||
}
|
||||
}
|
||||
|
||||
const createResult = await create()
|
||||
if (!createResult.ok) return newState
|
||||
const syncStackResult = await syncStack()
|
||||
if (!syncStackResult.ok) return newState
|
||||
const syncEnvResult = await syncEnvironment()
|
||||
if (!syncEnvResult.ok) return newState
|
||||
this.emit('newVisualizationConfiguration', [new Set(this.visualizationConfigs.keys())])
|
||||
await syncVisualizations()
|
||||
this.emit('visualizationsConfigured', [
|
||||
new Set(state.status === 'created' ? state.visualizations.keys() : []),
|
||||
])
|
||||
return newState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,10 @@ import type { Opt } from '@/util/data/opt'
|
||||
export type NonEmptyArray<T> = [T, ...T[]]
|
||||
|
||||
/** An equivalent of `Array.prototype.findIndex` method, but returns null instead of -1. */
|
||||
export function findIndexOpt<T>(arr: T[], pred: (elem: T) => boolean): number | null {
|
||||
export function findIndexOpt<T>(
|
||||
arr: T[],
|
||||
pred: (elem: T, index: number) => boolean,
|
||||
): number | null {
|
||||
const index = arr.findIndex(pred)
|
||||
return index >= 0 ? index : null
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user