Skip to content

tutorialStore onDocumentChanged unexpected behaviour #433

Open

Description

@so0k

Describe the bug

using the tutorialStore.onDocumentChanged triggers callback immediately because the nanostores subscribe callback seems to work that way.

For example:

    const terminal = tutorialStore.terminalConfig.value!.panels[1];
    const filePath = '/foo';
    tutorialStore.onDocumentChanged(filePath, (document) => {
       terminal.write(`${document.filePath} changed\n`);
    });

Expected comparison between old and new document to confirm changes happened

    const terminal = tutorialStore.terminalConfig.value!.panels[1];
    const filePath = '/foo';
    const stopListening = tutorialStore.documents.listen((newDocuments, oldDocuments) => {
      if (!newDocuments[filePath] || !oldDocuments || !oldDocuments[filePath]) {
        return;
      }
      const oldDocument = oldDocuments[filePath];
      const newDocument = newDocuments[filePath];
      if (oldDocument.value !== newDocument.value) {
        queueMicrotask(() => {
          terminal.write(`${newDocument.filePath} changed\n`);
          stopListening();
        });
      }
    });

Link to a StackBlitz project which shows the error

https://stackblitz.com/~/github.com/so0k/tutorialkit-terminal-writer

Steps to reproduce

  1. click "Test" button
  2. Notice terminal shows "Document changed" when it did not

Expected behavior

  1. click "Test" Button
  2. Modify foo (i.e. use TutorialKit API to update foo)
  3. only then, notice callback is triggered

Screenshots

No response

Platform

  • TutorialKit version: 1.3.1
  • OS: local Linux, Stackblitz
  • Browser: Chrome
  • Version: 131.0.6778.204 (Official Build) (64-bit)

Additional context

  1. Consider using listen if we don't want immediate callback?
  2. Compare newValue(s) to oldValue(s) before triggering?

Activity

  1. AriPerkkio commented on Jan 13, 2025

    @AriPerkkio
    Contributor

    because the nanostores subscribe callback seems to work that way.

    This seems to be the root cause indeed:

    store.subscribe(cb) in contrast with store.listen(cb) also call listeners immediately during the subscription.

    https://github.com/nanostores/nanostores?tab=readme-ov-file#atoms

    onDocumentChanged(filePath: string, callback: (document: Readonly<EditorDocument>) => void) {
    const unsubscribeFromCurrentDocument = this.currentDocument.subscribe((document) => {
    if (document?.filePath === filePath) {
    callback(document);
    }
    });
    const unsubscribeFromDocuments = this.documents.subscribe((documents) => {
    const document = documents[filePath];
    /**
    * We grab the document from the store, but only call the callback if it is not loading anymore which means
    * the content is loaded.
    */
    if (document && !document.loading) {
    /**
    * Call this in a `queueMicrotask` because the subscribe callback is called synchronoulsy,
    * which causes the `unsubscribeFromDocuments` to not exist yet.
    */
    queueMicrotask(() => {
    callback(document);
    unsubscribeFromDocuments();
    });
    }
    });

    The onDocumentChanged is not used internally by TutorialKit, it's only available via the experimental API. Fixing this should be quite risk-free.

  2. added
    bugSomething isn't working
    pr-welcomeAccepting pull requests for this change
    on Jan 13, 2025
  3. so0k commented on Dec 16, 2025

    @so0k
    Author

    seems it's not a bug in TutorialKit but in the way I'm watching the filesystem in my custom component... I'm upgrading stuff and decided to ask Claude Code Opus to have a look:

    The issue is that when navigating between lessons, the documents store is completely replaced and lessonFullyLoaded transitions. Your listener in RunSynth.tsx may be set up before the new lesson's filesystem is ready, or it's comparing against stale oldDocuments.

    The fix should check lessonFullyLoaded before setting up the listener, or reset the listener when the lesson changes. Here's an improved version:

    ● Update(src/components/RunSynth.tsx)
      ⎿  Updated src/components/RunSynth.tsx with 10 additions                         
            8        const terminal = tutorialStore.terminalConfig.value!.panels[1];
            9        terminal.input('npm run synth\n');
           10        const filePath = '/cdktf.out/stacks/workshop/cdk.tf.json';
           11 +  
           12 +      // Capture the current lesson ref to detect if lesson changes during synth
           13 +      const currentRef = tutorialStore.ref.value;
           14 +  
           15        const stopListening = tutorialStore.documents.listen((newDocuments, oldDocuments) => {
           16 +        // Abort if lesson has changed
           17 +        if (tutorialStore.ref.value !== currentRef) {
           18 +          stopListening();
           19 +          return;
           20 +        }
           21 +  
           22          if (!newDocuments[filePath] || !oldDocuments || !oldDocuments[filePath]) {
           23            return;
           24          }
    
  4. reopened this on Dec 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpr-welcomeAccepting pull requests for this change

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions