Monaco-editor: javascript/typescript typedefs lost when webworker times out

Created on 7 Jun 2017  路  10Comments  路  Source: microsoft/monaco-editor

monaco-editor version: 0.8.3 (playground)
Browser: Chrome 58.0.3029.110 (64-bit)
OS: Windows 10

When typescript is set to use commonjs modules, and the additional modules are provided as other editor models, the editor will handle imports correctly up until the typescript worker times out.
Once the typescript worker times out, the old models will not be taken into account by the new worker.

To reproduce:
Run this in the monaco playground.
Mouse over the "typed", "direct", and "impl" variables and note that they have typing info.
Open the chrome debug console, and make a note that typescript has a web worker present.
Move focus back to the playground code window, and go do something else for 5 minutes.
After the idle time has been reached, the typescript web worker will be disposed. Note its absence in the debug console.
Run the custom "Debug Monaco: get models" action, and note that the models still exist.
Try to mouse over the variables again. The webworker will be re-created, but the variables now show up as the "any" type, indicating that the models are being ignored.

const useLanguage = "typescript";

monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
    "target": monaco.languages.typescript.ScriptTarget.ES5,
    "moduleResolution": monaco.languages.typescript.ModuleResolutionKind.Classic,
    "module": monaco.languages.typescript.ModuleKind.CommonJS,
    "noEmit": true,
    "noImplicitAny": true,
    "noErrorTruncation": true,
    "suppressExcessPropertyErrors": false,
    "suppressImplicitAnyIndexErrors": true,
    "noLib": true,
    "allowJs": true
});

monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
    noSemanticValidation: false,
    noSyntaxValidation: false
});


function createDef(name, code) {
    const mod = monaco.editor.createModel(code, useLanguage, monaco.Uri.file("" + name));
    mod.onWillDispose(() => {
        console.log("Disposing typedef model", name);
    });
}

createDef(
    'directmod.d.ts', `
    declare module "directmod" {
        export = 42;
    }
    `
);

createDef(
    'implicitmod.js', `
    /**
     * 
     * @param {string} str - A string
     * @returns {number} The length of the string
     */
    function someFunc(str) {
        return str.length;
    }

    module.exports = {
        someFunc
    };
    `
);

createDef('typedefmod.d.ts', `
export function foo(): void;
`);


function createModel() {
    return monaco.editor.createModel(
`
const typed = require("typedefmod");
const direct = require("directmod");
const impl = require("implicitmod");

a.foo();
b.someFunc();
`,
        useLanguage,
        monaco.Uri.file("src/usage.js")
    );
}

const editor = monaco.editor.create(document.getElementById("container"), {
    model: createModel()
});

editor.addAction({
    id: "get-models",
    label: "Debug Monaco: Get Models",
    run: () => {
        alert(monaco.editor.getModels().map(x => x.uri.toString()).join("\n"))
    }
});

Background:
I have multiple commonjs-module javascript libraries that the user can edit. I am trying with reasonable success to enable autocomplete and other typed features by keeping the other libraries around as models. However, after the worker times out, all such typing features are lost and the types revert to "any".

monaco-typescript

Most helpful comment

Workaround:

Convert all non-editing models to typescriptDefaults.addExtraLib() entries, but still use file URIs for the "fileName" field. This bypasses the model / mirror model synchronization altogether and injects them straight into monaco-typescript's ts service. Thankfully, monaco-typescript will pass the fileName along to typescript itself, so the commonjs module resolution remains intact.

I did some digging, and I suspect the issue lies with EditorModelManager, specifically the third constructor argument "keepIdleModels". This controls the presence of a timer that makes it 'forget' about a model that has not been touched in a few minutes. However, there is no way to affect its behavior, as EditorWorkerClient._getOrCreateModelManager always passes false for this value.

All 10 comments

More information:

After the timeout happens, the models are not returned by the new worker's IWorkerContext, as can be seen if you call getScriptFileNames after the timeout occurs.

Below is the code for an action that will call this from the typescript worker.

When called while typedefs are available, the output will be the same as with "Debug Monaco: get models" in the original post.

When called after the timeout occurs, the output incorrectly shows only the editor's current model, and not the additional models.

editor.addAction({
    id: "get-mirror-model-uris",
    label: "Debug monaco: Get worker models",
    run: () => {
        const modelUri = editor.getModel().uri.toString();
        const ts = monaco.languages.typescript;
        ts.getTypeScriptWorker().then(worker => {
            return worker(modelUri);
        }).then(worker => {
            return worker.getScriptFileNames();
        }).then(files => {
            alert("Worker file names:\n" + files.join("\n"));
        });
    }
});

Workaround:

Convert all non-editing models to typescriptDefaults.addExtraLib() entries, but still use file URIs for the "fileName" field. This bypasses the model / mirror model synchronization altogether and injects them straight into monaco-typescript's ts service. Thankfully, monaco-typescript will pass the fileName along to typescript itself, so the commonjs module resolution remains intact.

I did some digging, and I suspect the issue lies with EditorModelManager, specifically the third constructor argument "keepIdleModels". This controls the presence of a timer that makes it 'forget' about a model that has not been touched in a few minutes. However, there is no way to affect its behavior, as EditorWorkerClient._getOrCreateModelManager always passes false for this value.

Indeed, the core EditorModelManager is used by multiple language services. e.g. css, html, json, typescript. For most of these languages (e.g. css) there is no need to synchronize all css models to the web worker when doing operations in one css model.

And models will be "forgotten" until a new request affects them, i.e. until someone asks the language service something about them. This is to save memory usage and IMHO makes good sense.

Perhaps the TypeScript language service could decide to exercise the flag that it doesn't save memory and once a model has been sent to a worker, it is never "forgotten" until the model is disposed.

Implement as option of the defaults, call monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true) to enable eager model sync whenever a worker is created or recreated.

I had a problem with the worker losing access to models which I used for imports' code completion even with setEagerModelSync set to true. The thing was that it fixes the problem only when the worker restarts, and the default timeouts for that and forgetting unused models differed (120s vs 30s).

I fixed that setting monaco.languages.typescript.typescriptDefaults.setMaximunWorkerIdleTime as well to the same timeout as the timer:

monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);
monaco.languages.typescript.typescriptDefaults.setMaximunWorkerIdleTime(30 * 1000);

Docs read:

/// monaco.d.ts

/**
 * Configure if all existing models should be eagerly sync'd
 * to the worker on start or restart.
 */
setEagerModelSync(value: boolean): void;

(...)

/**
 * Configure when the worker shuts down. By default that is 2mins.
 *
 * @param value The maximum idle time in milliseconds. Values less than one
 * mean never shut down.
 */
setMaximumWorkerIdleTime(value: number): void;

The timer set in EditorModelManager class seems to fire after STOP_SYNC_MODEL_DELTA_TIME_MS / 2 ms, which is:

/// vs/editor/common/services/editorWorkerServiceImpl.ts

/**
 * Stop syncing a model to the worker if it was not needed for 1 min.
 */
const STOP_SYNC_MODEL_DELTA_TIME_MS = 60 * 1000;

@KayoMaciek

I had the same issue that my imported TypeScript models disappeared (got import errors) after 90 sec.

But in my case not even a setEagerModelSync(true) + setMaximumWorkerIdleTime(30 * 1000) helped :/

I had to completely disable the _checkStopModelSync() timeout call (happening every 30 secs) to avoid that it calls _stopModelSync() for all the unused models at the third callback (after 90 sec).

@alexandrudima: Is it okay to disable this timer? If yes, it would be cool if the Monaco editor API exposes the boolean keepIdleModels c'tor parameter

I had to completely disable the _checkStopModelSync() timeout call (happening every 30 secs) to avoid that it calls _stopModelSync() for all the unused models at the third callback (after 90 sec).

@ulrichb Thanks for sharing! Got any pointers on how to disable the _checkStopModelSync() call?

@skymakerolof

(As a hackaround) I'm overwriting/decorating EditorWorkerClient.prototype._getOrCreateModelManager, calling the original function and then modifying the returned EditorModelManager instance (set a dummy _checkStopModelSync function).

I've also created a repro for my scenario, see #1451.

Thanks for the tip @ulrichb - your suggested hackaround worked for me (I needed to overwrite _checkStopModelSync and _checkStopIdleWorker functions on 0.15.6)

Was this page helpful?
0 / 5 - 0 ratings