datacore icon indicating copy to clipboard operation
datacore copied to clipboard

Intermittent failure with a Datacore JSX code block

Open tosascott opened this issue 6 months ago • 3 comments

I am seeing an intermittent failure with a Datacore JSX code block on two different Macs as well as on an iPhone. The error can be present when switching to the note. It can also spontaneously happen when the note is open even when I am not editing it. A refresh -- via navigating away and back or using the Refresh Any View plugin -- usually clears the error message.

TypeError: d2 is not a function or its return value is not iterable
    at useIndexUpdates (plugin:datacore:34705:35)
    at tryUseFullQuery (plugin:datacore:34724:25)
    at useFullQuery (plugin:datacore:34749:10)
    at useQuery (plugin:datacore:34752:10)
    at DatacoreLocalApi.useQuery (plugin:datacore:37424:12)
    at Children (eval at evalInContext (plugin:datacore), <anonymous>:59:6)
    at Object.value (eval at evalInContext (plugin:datacore), <anonymous>:98:45)
    at eval (plugin:datacore:35426:33)
    at T2 (plugin:datacore:13900:37)
    at x.TableRowCell [as constructor] (plugin:datacore:35426:17)

While I am a JavaScript novice, I'm at a loss as to how my code could corrupt the function lookup table such that Datacore's d2() function is lost.

The desktop app version 1.9.3 (Installer 1.7.7), with a Catalyst license, is installed on both an M4 Mac mini and an older MacBook Pro running Sequoia 15.5. It also happens on a TestFlight managed install on an iPhone.

I am using Datacore 0.1.24 managed by BRAT. Not sure if it's relevant, but I also have Dataview 0.5.68 installed.

The problem has been happening for several months across multiple versions of Obsidian, Datacore, and a bunch of other plugins. I have rebuilt the vault cache multiple times.

The code block is

```datacorejsx
const { RelatedNotes } = await dc.require("scripts/RelatedNotes.jsx")
return <RelatedNotes />;
```

The RelatedNodes.jsx code uses Datacore to create a fancy MOC. There are three optional tables. The Timeline Notes table is a chronologically sorted list of uniquely named notes which declare this note as their parent in the properties block. (As you'll see if you look at the code, the unique notes are in a directory called Beads.) The Child Notes table contains non-time-based notes which also are children. The final Linking Notes table enumerates other notes with inline links.

In the first two tables, any tasks that are open, in-progress, are a question (i.e., - [?] …) or starred (i.e., - [*] …), are included in the table to allow for a rapid review.

What it is supposed to look like:

Image

I had to rename the JSX file to have a .txt suffix in order to attach it to this issue report:

RelatedNotes.jsx.txt

tosascott avatar Jun 27 '25 20:06 tosascott

  1. you're not supposed to directly call a react component. KidsTable and LinksTable are functional components.

  2. component functions should not contain more than 1 parameter. the first and only parameter is a props object, which can (and should) be destructured into named parameters.

// From <https://blacksmithgu.github.io/datacore/quickstart>
//
// All datacore views should return a React component; in practice, this is
// going to be something that looks like HTML. That's enabled by using JSX instead of stock JavaScript.
//

// XXX - At some point, see if we can use useMemo to avoid quote replacement
// 	for a path we've seen before
//
// import { useMemo } from "preact/hooks";
function EscapeQuotes(path) {
  // Escape double quotes
  // const escaped = useMemo(() => path.replaceAll('"', '\\"'), [path]);
  // return escaped;
  return path.replaceAll('"', '\\"');
}

// Much of the time, we want to report both the page and its tasks of interest.
function PageWithTasks(page) {
  let result = String(page.$link);
  const safePath = EscapeQuotes((path = page.$path));
  const tasks = dc.useQuery(
    `@task
	  and $file = "${safePath}"`,
    // and $completed = false`,
  );
  // With extended task status a simple completed or not test is not
  // sufficient. We explicitly test for the following status characters.
  const interestingStatus = [
    " ", // yet to be started
    "/", // a work in progress
    "*", // a starred task
    "?", // an open question
  ];
  for (const task of tasks) {
    if (interestingStatus.includes(task.$status)) {
      result += `\n- [${task.$status}] ${task.$text}`;
    }
  }
  return result;
}

// Sometimes we want to include a page along with each of the tasks
// of interest. When we don't want those tasks, call this variation.
function PageWithoutTasks(page) {
  const result = String(page.$link);
  return result;
}

function Children(page) {
  // This trick was found in the Discord support channel. None of the
  // documentation I found showed the need for nesting linksto() and
  // id(). See
  // https://discord.com/channels/1219902517304098836/1248851728137195591/1253112341252276266
  const safePath = EscapeQuotes(page.$path);
  const kids = dc
    .useQuery(`@page and linksto(id("${safePath}"))`)
    .filter((entry) => entry.$link);

  const result = kids.length > 0 ? kids.length : "";
  return result;
}

function BeadTable({safePath}) {
  const BEAD_COLUMNS = [
    //{ id: "N", value: (page) => RowIndex() },
    { id: "Timeline Notes", value: (page) => PageWithTasks((page = page)) },
    // { id: "Sublinks", value: (page) => Children((page = page)) },
  ];
  const beads = dc
    .useQuery(
      `@page
        and path("Beads")
        and linksto(id("${safePath}"))`,
      //
      // As of 2025-03-30 removed the restriction that if there is a link to
      // the page elsewhere then don't included it in the Named Notes section.
      // While that sort of makes sense, being able to scan the Name Notes
      // for ALL children is mighty handy.
      //      ` and !linkedfrom(id("${safePath}"))`
    )
    .filter((entry) => entry.$link);
  const sortedBeads = dc.useArray(beads, (array) =>
    array.sort((page) => page.$name, "desc"),
  );
  return sortedBeads.length > 0 ? (
    <dc.Stack>
      <dc.VanillaTable columns={BEAD_COLUMNS} rows={sortedBeads} />
    </dc.Stack>
  ) : null;
}

function KidsTable({safePath, currentFile}) {
  const LINKS_COLUMNS = [
    { id: "Child Notes", value: (page) => PageWithTasks((page = page)) },
    { id: "Grandchildren", value: (page) => Children((page = page)) },
  ];
  const links = dc
    .useQuery(
      `@page
        and !path("Beads")
        and !path("journal")
        and !path("Templates")
        and contains(parent, [[${currentFile.$name}]])`,
      //
      // As of 2025-03-30 removed the restriction that if there is a link to
      // the page elsewhere then don't included it in the Named Notes section.
      // While that sort of makes sense, being able to scan the Name Notes
      // for ALL children is mighty handy.
      //      ` and !linkedfrom(id("${safePath}"))`
    )
    .filter((entry) => entry.$link);
  const sortedLinks = dc.useArray(links, (array) =>
    array.sort((page) => page.$name, "asc"),
  );
  return sortedLinks.length > 0 ? (
    <dc.Stack>
      <dc.VanillaTable columns={LINKS_COLUMNS} rows={sortedLinks} />
    </dc.Stack>
  ) : null;
}

function LinksTable({safePath, currentFile}) {
  const LINKS_COLUMNS = [
    { id: "Linking Notes", value: (page) => PageWithoutTasks((page = page)) },
    // { id: "Sublinks", value: (page) => Children((page = page)) },
  ];
  const links = dc
    .useQuery(
      `@page` +
        ` and !path("Beads")` +
        ` and !path("journal")` +
        ` and !path("Templates")` +
        ` and linksto(id("${safePath}"))` +
        ` and !contains(parent, [[${currentFile.$name}]])`,
      //
      // As of 2025-03-30 removed the restriction that if there is a link to
      // the page elsewhere then don't included it in the Named Notes section.
      // While that sort of makes sense, being able to scan the Name Notes
      // for ALL children is mighty handy.
      //      ` and !linkedfrom(id("${safePath}"))`
    )
    .filter((entry) => entry.$link);
  const sortedLinks = dc.useArray(links, (array) =>
    array.sort((page) => page.$name, "desc"),
  );
  return sortedLinks.length > 0 ? (
    <dc.VanillaTable columns={LINKS_COLUMNS} rows={sortedLinks} />
  ) : null;
}

const ArrowFunc_RelatedNotes = (title = <h2>Related Notes</h2>) => {
  const currentFile = dc.useCurrentFile();
  const safePath = EscapeQuotes(currentFile.$path);

  const beads_elements = <BeadTable safePath={safePath}/>;
  const links_elements = <LinksTable safePath={safePath} currentFile={currentFile}/>;

  return (
    <div>
      <p>title = {title}.</p>
      <hr />
      {title}
      {beads_elements}
      {links_elements}
    </div>
  );
};

// Invocation options
//
// 	return RelatedNotes;
//
// 	return <RelatedNotes />;
//
// 	return <RelatedNotes title=<hr/> />;
//
// 	return <RelatedNotes title=<h3>Smaller title</h3> /> />;

function RelatedNotes({ title = <h2>Related Notes</h2> }) {
  const currentFile = dc.useCurrentFile();
  const safePath = EscapeQuotes(currentFile.$path);

  const beads_elements = <BeadTable safePath={safePath}/>;
  const kids_elements = <KidsTable safePath={safePath} currentFile={currentFile}/>;
  const links_elements = <LinksTable safePath={safePath} currentFile={currentFile}/>;
  let alternate = null;

  if (!beads_elements && !links_elements && !kids_elements) {
    alternate = <p>No related notes were found</p>;
  }

  return (
    <div>
      {title}
      {beads_elements}
      {kids_elements}
      {links_elements}
      {alternate}
    </div>
  );
}

return { RelatedNotes };

GamerGirlandCo avatar Jun 29 '25 04:06 GamerGirlandCo

@GamerGirlandCo, thank you for taking the time to look at my code. Since it worked some of the time, I thought I had found a bug. Now that I know the error is between the keyboard and chair, I'll spend some quality time with MDN docs on React, JSX, and JavaScript as I refactor the code.

tosascott avatar Jul 02 '25 02:07 tosascott

TL;DR: nested use of dc.useQuery(…) leads to the exception "TypeError: d2 is not a function or its return value is not iterable". Switching one of the calls to dc.query(…) allows for dc.Table() to work properly. Detecting that programming error and notifying the user, or updating the documentation, would be nice.


After refactoring my code to properly create JSX components and props as recommended by @GamerGirlandCo, the problem remained. As you'll see in the attached, stripped-down test case for <BugReport113 />, I determined I was incorrectly using dc.useQuery(…) both to find notes of interest and to then find how many notes in turn linked to them.

To test add BugReport113.jsx to your vault and then create a note with the following three codeblocks. Each codeblock has a different setting for testOption. Then create a second note that links to the first. Go back to the first note, start to edit it, and the testOption="nested" code block will show the exception.


## Single useQuery succeeds
```datacorejsx
const { BugReport113 } = await dc.require("scripts/BugReport113.jsx")
return <BugReport113 testOption="single" />;
```

## Mix of query() and useQuery() succeeds
```datacorejsx
const { BugReport113 } = await dc.require("scripts/BugReport113.jsx")
return <BugReport113 testOption="mixed" />;
```

## Nested useQuery() calls fail
```datacorejsx
const { BugReport113 } = await dc.require("scripts/BugReport113.jsx")
return <BugReport113 testOption="nested" />;
```

The failure looks like:

Image

BugReport113.jsx.txt

tosascott avatar Jul 04 '25 20:07 tosascott