How to keep a notebook working

Hi,
I have a simple notebook that was running perfectly and today no more.
It is really annoying. Demo time.
Resources (from require) seems available yet.

Any help would be welcomed.

Tested load of 2 ressources. ganja is ok, leaflet.sync not.
Both available.
What do I have missed ?
From Introduction to require / Observable | Observable

a = require(“https://cdn.jsdelivr.net/npm/leaflet.sync@0.2.4/L.Map.Sync.min.js”)

Both exist and available from https://www.jsdelivr.com

1 Like

Sorry to hear that! Just so I understand… are you saying that the leaflet.sync require statement worked before and now it is not working? I see the same behavior atm, but wondering if it actually worked before…

Yes Sure , it was working.
This notebook is a simple test for more complicated ones that do not work anymore.

for example
or

Same issue: map = TypeError: L.control.resizer is not a function
because require does not load the js ressource anymore.

In your L cell, change

return L;

to

return window.L;

Indeed. Thanks…

Why this change now ?

And why the require on leaflet.sync@0.2.4/L.Map.Sync.min.js does not work .

It never did, you just forgot the .catch(). Take a look at https://cdn.jsdelivr.net/npm/leaflet.sync@0.2.4/L.Map.Sync.js and you’ll see that there’s no UMD support. The file is meant to be included directly in the browser and expects the global variable L to be present.

By calling

require('https://cdn.jsdelivr.net/npm/leaflet.sync@0.2.4/L.Map.Sync.min.js').catch(()=>{})

you tell d3-require to attempt to load the script, and suppress any errors. d3-require creates a script element with the script URL as src, attaches it to the document, waits for the UMD module to register itself (via window.define()), fails, and throws the error “invalid module”. .catch() suppresses the error.

This pattern is often used in combination with, e.g., .catch(() => window.Foo), where Foo is a global variable created by the loaded script. This allows other cells to 1. wait for the library to load, and 2. reference it via its loading cell, instead of via window.

3 Likes

Ok I will try to make those things clearer.

But according to the doc from Introduction to require / Observable / Observable, I understand that wrongly.

What about the change now needed window.L
Because I can assure you that those notebooks were running correctly.

Could you help me to correct loading of ressources needed from Leaflet Synchro / Patrick Brockmann / Observable ?

No idea, tbh. For this to work in the past, cells would have to have received the global variable instead of the cell value whenever a global var of the same name was defined.

@mbostock might be able to provide an answer.

@PBrockmann This seems to do the trick:

L = {
  const BASE = `https://cdn.jsdelivr.net/npm/`;
  const style = document.head.appendChild(html`<style>
    @import url(${BASE}leaflet@1.7.1/dist/leaflet.css);
    @import url(${BASE}leaflet-fullscreen@1.0.2/dist/leaflet.fullscreen.css);
    @import url(${BASE}leaflet-timedimension@1.1.1/dist/leaflet.timedimension.control.min.css);
    @import url(${BASE}leaflet.control.resizer@0.0.1/L.Control.Resizer.css);
  `);
  invalidation.then(() => style.remove());

  delete window.L;
  const L = window.L = await require(`${BASE}leaflet@1.7.1/dist/leaflet-src.min.js`);
  await Promise.all([
    `${BASE}leaflet-fullscreen@1.0.2`,
    `${BASE}leaflet-timedimension@1.1.1`,
    `${BASE}leaflet.sync@0.2.4`,
    `${BASE}leaflet.control.resizer@0.0.1`
  ].map(url => require(url).catch(() => {})));
  
  return L;
}

Unfortunately there doesn’t seem to be a cleaner way, as leaflet cannot be extended by the plugins if it’s loaded as an ES module.

One of the problems here is that multiple versions of Leaflet are being loaded. At the top-level, you’ve requested version 1.7.1, but one of the other libraries you’re loading has a transitive dependency on leaflet, too, resulting in the latest version 1.8.0 also being loaded. I put your libraries into the package dependency visualizer here:

If you want to load a complicated network of dependencies like this, your best bet is to use require.alias to define all of the versions you want to load explicitly. As an added bonus, if you define the dependencies explicitly, require won’t have to load the package.json files to resolve the versions, so everything will load faster. And you can also load things faster by using Promise.all to load in parallel.

L = {
  const r = require.alias({
    "leaflet": "leaflet@1.7.1/dist/leaflet-src.js",
    "leaflet/dist/leaflet.css": "leaflet@1.7.1/dist/leaflet.css",
    "leaflet-fullscreen": "leaflet-fullscreen@1.0.2/dist/Leaflet.fullscreen.min.js",
    "leaflet-fullscreen/dist/leaflet.fullscreen.css": "leaflet-fullscreen@1.0.2/dist/leaflet.fullscreen.css",
    "leaflet-timedimension": "leaflet-timedimension@1.1.1/dist/leaflet.timedimension.min.js",
    "leaflet-timedimension/dist/leaflet.timedimension.control.min.css": "leaflet-timedimension@1.1.1/dist/leaflet.timedimension.control.min.css",
    "iso8601-js-period": "iso8601-js-period@0.2.1/iso8601.js",
    "leaflet.sync": "leaflet.sync@0.2.4/L.Map.Sync.js",
    "leaflet.control.resizer": "leaflet.control.resizer@0.0.1/L.Control.Resizer.js",
    "leaflet.control.resizer/L.Control.Resizer.css": "leaflet.control.resizer@0.0.1/L.Control.Resizer.css"
  });
  const L = await r("leaflet");
  await Promise.all([
    r("leaflet-fullscreen").catch(() => {}),
    r("leaflet-timedimension").catch(() => {}),
    r("leaflet.sync").catch(() => {}),
    r("leaflet.control.resizer").catch(() => {})
  ]);
  if (!L._style) { // apply stylesheets (only once on load)
    L._style = true;
    document.head.append(htl.html`
      <link href=${await r.resolve("leaflet/dist/leaflet.css")} rel=stylesheet>
      <link href=${await r.resolve("leaflet-fullscreen/dist/leaflet.fullscreen.css")} rel=stylesheet>
      <link href=${await r.resolve("leaflet-timedimension/dist/leaflet.timedimension.control.min.css")} rel=stylesheet>
      <link href=${await r.resolve("leaflet.control.resizer/L.Control.Resizer.css")} rel=stylesheet>
    `);
  }
  return L;
}

https://observablehq.com/compare/4cbfcfc2d8cd3222...c43119f41bf860c0

There are lots of tricky things here:

  1. The leaflet module is UMD, so it works great with require. But the various plugins do not; they are simply vanilla JavaScript, pre-ES modules, and they expect the leaflet module to define the L global. This is why you must await r("leaflet") before loading the other modules, rather than being able to load them in parallel.
  2. The leaflet plugins mutate the L global, so you don’t need the return value resulting from loading them. An empty catch is enough to ignore the error (since these modules don’t implement AMD/UMD).
  3. Leaflet and its plugins also use global stylesheets, but do not insert them into the document automatically, so you need to do it. Also, you want to avoid inserting them multiple times, ideally, if the L cell is re-run. I used a flag L._style to check whether I’ve already inserted the stylesheets to avoid this, but it also means that you might need to reload the notebook if you change the definition of L.
  4. When resolving the stylesheets, you want to be consistent with the version of the module you are using, so I included them in require.alias, too. I think that means that the subsequent require.resolve calls resolve synchronously, so it’s fine to await them in series; if these were really asynchronous, you’d want to include them in the Promise.all so they can be done in parallel, too.
5 Likes

Version 1.8.0 was just released a couple of days ago, which probably explains the observed change in behavior.

1 Like

First of all. Many thanks for having taken the time to make this really good support on this tricky problem when loading resources.

I have finally used leaflet 1.8.0 and have noticed no problem with it.

A problem still persists when I use the Export button (after having added 2 maps).

image

I get this message in the new thumbnail. If I reload the page, then the maps appear.
Sometimes also after a reload of the notebook, the problem is visible.

image

Do not know how to solve this problem again. Sorry.

Can you share a notebook with the latest problem? Thanks.

No, no this is me that feel bitterly in request.

To run properly you must first access to the thredds server from a click to https://www.globalcarbonatlas.org:8443/thredds/catalog/Atlas/Flux_Transcom/catalog.html.

Yeah, so the problem is that leaflet-timedimension implements UMD, which is good!, but means you can’t use the same catch pattern you’re using for the other Leaflet plugins that don’t implement UMD. Instead of this:

    r("leaflet-timedimension").catch(() => {}),

You need this:

    r("leaflet-timedimension").then((o) => (L.TimeDimension = o)),

Otherwise, you’re loading leaflet-timedimension, but you’re not assigning it to the L object, so the subsequent L.TimeDimension call fails.

I discovered this by looking at the source:

https://cdn.jsdelivr.net/npm/leaflet-timedimension@1.1.1/dist/leaflet.timedimension.src.js

On the more general subject of keeping notebooks working (the thread title), I use a healthcheck notebook

That notebook can be called via a URL which when called runs the healthcheck on an arbitrary notebook.

So then I automate calling the healtcheck using a monitoring service (uptime robot).

The entire technique is explained:

Here is my personal status dashboard:

I am half thinking of streamlining this process to create a subscription monitoring service for like $10 a month and it will auto monitor all public notebooks for errors, would you be interested?

1 Like

I have tried this but get sometimes:

image

That is tricky and puzzling…

Any news on this issue.
The “export map” produces this error.
If I insist by reloading and reloading. it works.
So a problem again when loading ressources.

You could try ignoring errors on the leaflet-timedimension require:

    r("leaflet-timedimension").then((o) => (L.TimeDimension = o), () => {}),

But I think the real answer is probably that you should transpile and bundle Leaflet and all the plugins into a modern ES module so that you can load it without all these race conditions. :grimacing: Given that each of these Leaflet plugins uses a different style of export (which is perfectly representative of the insanity solved by the ES modules standard), I’m not sure it’s possible to load this set of libraries reliably using require.