feat(frontend): global recompute helper function (#5408)

* feat(frontend): global recompute helper function

* fixes

* remove unused import

* filteredIds -> excludeIds

* get(recomputeAllContext)?

* deal with circulare recomputes

* consistent naming
This commit is contained in:
Alexander Petric
2025-03-04 19:35:35 +01:00
committed by GitHub
parent 9abaf6cec3
commit b961efa869
7 changed files with 62 additions and 30 deletions
@@ -1,5 +1,6 @@
<script lang="ts">
import { createEventDispatcher, getContext, onDestroy, tick } from 'svelte'
import { get } from 'svelte/store'
import type {
AppInput,
EvalAppInput,
@@ -32,7 +33,7 @@
export let onDemandOnly: boolean = false
export let exportValueFunction: boolean = false
const { componentControl, runnableComponents } = getContext<AppViewerContext>('AppViewerContext')
const { componentControl, runnableComponents, recomputeAllContext } = getContext<AppViewerContext>('AppViewerContext')
const editorContext = getContext<AppEditorContext>('AppEditorContext')
const iterContext = getContext<ListContext>('ListWrapperContext')
@@ -264,6 +265,7 @@
try {
const context = computeGlobalContext(
$worldStore,
id,
deepMergeWithPriority(fullContext, args ?? {})
)
const r = await eval_like(
@@ -275,7 +277,8 @@
$worldStore,
$runnableComponents,
false,
groupContext?.id
groupContext?.id,
get(recomputeAllContext)?.onRefresh
)
error = ''
return r
@@ -294,14 +297,15 @@
try {
const r = await eval_like(
'`' + input.eval.replaceAll('`', '\\`') + '`',
computeGlobalContext($worldStore, fullContext),
computeGlobalContext($worldStore, id, fullContext),
$state,
$mode == 'dnd',
$componentControl,
$worldStore,
$runnableComponents,
false,
groupContext?.id
groupContext?.id,
get(recomputeAllContext)?.onRefresh
)
error = ''
return r
@@ -78,7 +78,8 @@
selectedComponent,
app,
connectingInput,
bgRuns
bgRuns,
recomputeAllContext
} = getContext<AppViewerContext>('AppViewerContext')
const editorContext = getContext<AppEditorContext>('AppEditorContext')
@@ -286,7 +287,7 @@
try {
r = await eval_like(
runnable.inlineScript?.content,
computeGlobalContext($worldStore, {
computeGlobalContext($worldStore, id, {
iter: iterContext ? $iterContext : undefined,
row: rowContext ? $rowContext : undefined,
group: groupContext ? get(groupContext.context) : undefined
@@ -297,14 +298,23 @@
$worldStore,
$runnableComponents,
true,
groupContext?.id
groupContext?.id,
get(recomputeAllContext)?.onRefresh
)
await setResult(r, job)
$state = $state
} catch (e) {
sendUserToast(`Error running frontend script ${id}: ` + e.message, true)
r = { error: { message: e.body ?? e.message } }
let additionalInfo = ''
if (
e.message.includes('Maximum call stack size exceeded') ||
e.message.includes('too much recursion')
) {
additionalInfo =
'This is likely due to a call to globalRecompute() in the frontend script. Please check your script for circular recomputes and disable the "Run on start and app refresh" toggle.'
}
sendUserToast(`Error running frontend script ${id}: ` + e.message + additionalInfo, true)
r = { error: { message: (e.body ?? e.message) + additionalInfo } }
await setResult(r, job)
}
loading = false
@@ -519,7 +529,7 @@
raw.set(res)
const transformerResult = await eval_like(
transformer.content,
computeGlobalContext($worldStore, {
computeGlobalContext($worldStore, id, {
iter: iterContext ? $iterContext : undefined,
row: rowContext ? $rowContext : undefined,
group: groupContext ? get(groupContext.context) : undefined,
@@ -531,7 +541,8 @@
$worldStore,
$runnableComponents,
true,
groupContext?.id
groupContext?.id,
get(recomputeAllContext)?.onRefresh
)
return transformerResult
} catch (err) {
@@ -3,7 +3,7 @@ import { sendUserToast } from '$lib/toast'
import { waitJob } from '$lib/components/waitJob'
import { base } from '$lib/base'
export function computeGlobalContext(world: World | undefined, extraContext: any = {}) {
export function computeGlobalContext(world: World | undefined, id: string | undefined, extraContext: any = {}) {
return {
...Object.fromEntries(
Object.entries(world?.outputsById ?? {})
@@ -15,7 +15,8 @@ export function computeGlobalContext(world: World | undefined, extraContext: any
]
})
),
...extraContext
...extraContext,
id
}
}
@@ -26,7 +27,7 @@ function create_context_function_template(
) {
let hasReturnAsLastLine = noReturn || eval_string.split('\n').some((x) => x.startsWith('return '))
return `
return async function (context, state, createProxy, goto, setTab, recompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll, clearFiles, showToast, waitJob, askNewResource, downloadFile) {
return async function (context, state, createProxy, goto, setTab, recompute, globalRecompute, getAgGrid, setValue, setSelectedIndex, openModal, closeModal, open, close, validate, invalidate, validateAll, clearFiles, showToast, waitJob, askNewResource, downloadFile) {
"use strict";
${contextKeys && contextKeys.length > 0
? `let ${contextKeys.map((key) => ` ${key} = createProxy('${key}', context['${key}'])`)};`
@@ -49,6 +50,7 @@ type WmFunctor = (
goto,
setTab,
recompute,
globalRecompute,
getAgGrid,
setValue,
setSelectedIndex,
@@ -120,7 +122,8 @@ export async function eval_like(
worldStore: World | undefined,
runnableComponents: Record<string, { cb?: (() => void)[] }>,
noReturn: boolean,
groupContextId: string | undefined
groupContextId: string | undefined,
globalRecomputeFunction: ((excludeIds?: string) => void) | undefined
) {
const createProxy = (name: string, obj: any) => {
// console.log('Creating proxy', name, obj)
@@ -198,6 +201,12 @@ export async function eval_like(
(id) => {
runnableComponents[id]?.cb?.forEach((f) => f())
},
() => {
const callerId = ((context ?? {}) as any).id
if (callerId) {
globalRecomputeFunction?.(callerId)
}
},
(id) => {
return controlComponents[id]?.agGrid
},
@@ -268,9 +277,10 @@ export async function eval_like(
}
if (typeof input === 'object' && input.s3) {
const workspaceId = computeGlobalContext(worldStore).ctx.workspace
const s3href = `${base}/api/w/${workspaceId}/job_helpers/download_s3_file?file_key=${input?.s3
}${input?.storage ? `&storage=${input.storage}` : ''}`
const workspaceId = ((context ?? {}) as any).ctx?.workspace
const s3href = `${base}/api/w/${workspaceId}/job_helpers/download_s3_file?file_key=${
input?.s3
}${input?.storage ? `&storage=${input.storage}` : ''}`
downloadFile(s3href, filename || input.s3)
} else if (typeof input === 'string') {
if (input.startsWith('data:')) {
@@ -69,7 +69,7 @@
}
})
function onClick(stopAfterClear: boolean, source: string) {
function onRefresh(stopAfterClear: boolean, source: string, excludeId?: string) {
if (timeout) {
clearInterval(timeout)
timeout = undefined
@@ -81,7 +81,7 @@
if (stopAfterClear) return
}
if (firstLoad) {
refresh('onClick ' + source)
refresh('onClick ' + source, excludeId)
}
if ($recomputeAllContext.interval) {
@@ -109,11 +109,11 @@
function setInter(inter: number | undefined, source: string) {
$recomputeAllContext.interval = inter
onClick(!inter, 'setInter ' + source)
onRefresh(!inter, 'setInter ' + source)
}
let refreshing: string[] = []
function refresh(reason: string) {
function refresh(reason: string, excludeId: string | undefined = undefined) {
let isFirstLoad = false
if (!firstLoad && reason == 'all initialized') {
console.log('refresh all first load', reason)
@@ -129,8 +129,9 @@
const promises = Object.keys($runnableComponents)
.flatMap((id) => {
if (
!$runnableComponents?.[id]?.autoRefresh &&
(!isFirstLoad || !$runnableComponents?.[id]?.refreshOnStart)
excludeId === id ||
(!$runnableComponents?.[id]?.autoRefresh &&
(!isFirstLoad || !$runnableComponents?.[id]?.refreshOnStart))
) {
return
}
@@ -170,21 +171,21 @@
if (progressTimer) clearInterval(progressTimer)
}
} else if (shouldRefresh) {
timeout = setInterval(() => refresh('onClick interval'), $recomputeAllContext.interval)
timeout = setInterval(() => refresh('onRefresh interval'), $recomputeAllContext.interval)
startProgress()
}
}
onMount(() => {
$recomputeAllContext = {
onClick: () => onClick(false, 'allContext'),
onRefresh: (excludeIds) => onRefresh(false, 'allContext', excludeIds),
setInter: (n) => setInter(n, 'all context')
}
})
</script>
<RecomputeAllButton
on:click={() => onClick(false, 'button')}
on:click={() => onRefresh(false, 'button')}
interval={$recomputeAllContext.interval}
{refreshing}
componentNumber={$recomputeAllContext.componentNumber ?? 0}
@@ -192,6 +193,6 @@
progress={$recomputeAllContext.progress}
on:setInter={(e) => {
setInter(e.detail, 'button setInter')
onClick(false, 'button setInter')
onRefresh(false, 'button setInter')
}}
/>
@@ -40,7 +40,7 @@
<RecomputeAllButton
interval={$recomputeAllContext.interval}
componentNumber={$recomputeAllContext.componentNumber ?? 0}
on:click={() => $recomputeAllContext.onClick?.()}
on:click={() => $recomputeAllContext.onRefresh?.()}
on:setInter={(e) => {
$recomputeAllContext.setInter?.(e.detail)
}}
+1 -1
View File
@@ -284,7 +284,7 @@ export type AppViewerContext = {
policy: Policy
recomputeAllContext: Writable<{
onClick?: () => void
onRefresh?: (excludeId?: string) => void
componentNumber?: number | undefined
interval?: number | undefined
refreshing?: string[] | undefined
@@ -231,6 +231,12 @@ declare function setTab(id: string, index: number): void;
*/
declare function recompute(id: string): void;
/** recompute all components runnables and background runnables
* Make sure to disable the toggle "Run on start and app refresh" in the
* settings panel to avoid circular recomputes
*/
declare function globalRecompute(): void;
/** get the ag grid api from an AgGridTable
* @param id component's id
*/