Introduction
Pour mes notes personnelles, j’ai choisi l’application Obsidian.md car elle me permet de rédiger et d’organiser mes notes directement sur mon disque dur et de choisir le service de sauvegarde cloud.
Cependant, Obsidian ne fournit pas de fonction de base de données telle que proposé par d’autres applications comme Notion.so ou Confluence.
Je me suis alors interrogé sur l’intégration du moteur de base de données SQLite sous forme de plugin dans Obsidian.
Mon choix s’est porté sur SQLite car la base de données est stockée dans un fichier unique. Cela m’offre l’avantage d’avoir mon fichier de base de données côte à côte avec mes notes dans mon coffre Obsidian.
Choix techniques
Pour intégrer SQLite, je choisis la bibliothèque better-sqlite3, reconnue pour sa popularité et sa maintenance active. Elle fournit le moteur SQLite sous la forme d’une extension binaire pré-compilée pour Node.js.
Obsidian.md étant développée avec la plateforme Electron, l’environnement d’exécution Node.js est accessible et permet donc le chargement de cette extension native.
Il est techniquement possible d’exécuter SQLite dans le moteur de rendu Chromium intégré à Electron.
Cependant, je pense qu’il est préférable d’exécuter les opérations de base de donnés dans un processus d’arrière plan pour ne pas surchargé le moteur de rendu.
Contraintes techniques
Pour que l’extension native SQLite puisse fonctionner sur toutes les plateformes supportées (Windows, Mac, Linux), nous devons mettre en place un mécanisme de détection et de téléchargement de l’extension en fonction de l’environnement d’exécution.
Analyse de faisabilité
Les chapitres suivants décrivent les étapes de validation de la faisabilité du projet.
Démarrer un processus enfant Node.js
Un plugin Obsidian s’exécute dans le moteur de rendu Chromium. En explorant les fonctionnalités API
accessibles, j’ai découvert que nous avions accès à electron ainsi qu’à l’objet utilityProcess
qui nous permet de démarrer un processus enfant avec le moteur d’exécution Node.js.
const child = electron.remote.utilityProcess.fork(
'<absolute-path>/my-code.js',
{ serviceName: 'obsidian-db-service', stdio: 'pipe' }
Communication IPC
En utilisant utilityProcess.fork
, electron ouvre automatiquement les canaux de communication inter-processus. Ce qui nous permet d’échanger des messages entre le moteur de rendu Chromium et le processus d’arrière plan Node.js.
child.postMessage({ data: "hello from Chromium" });
child.on("message", (message) => {
process.parentPort?.on("message", (message) => {
process.paentPort?.postMessage("hello from Node.js");
Les plugin Obsidian sont chargés et exécuté par le moteur de rendu Chromium. Par conséquent, on ne peut pas compter sur un gestionnaire de paquet pour installer l’extension native requise. Pour gérer cette contrainte, je me suis inspiré des méthodes de distribution utilisées par les développeurs de better-sqlite3
.
Dans le code source de la bibliothèque, on constate qu’ils utilisent prebuild-install
pour le téléchargement du binaire et bindings
pour le chargement de l’extension.
if (nativeBinding == null) {
(DEFAULT_ADDON = require("bindings")("better_sqlite3.node"));
} else if (typeof nativeBinding === "string") {
// See <https://webpack.js.org/api/module-variables/#__non_webpack_require__-webpack-specific>
typeof __non_webpack_require__ === "function"
? __non_webpack_require__
path.resolve(nativeBinding).replace(/(\.node)?$/, ".node")
// See <https://github.com/WiseLibs/better-sqlite3/issues/972>
"install": "prebuild-install || node-gyp rebuild --release"
Pour que mon plugin puisse fonctionner sur tous les systèmes d’exploitations supportés par Obsidian. Il me faut implémenter des mécanismes de téléchargement et de chargement similaires.
Mécanisme de téléchargement
Le téléchargement du binaire s’effectue grâce au module prebuild-install
qui implémente cette logique en deux étapes qu’on retrouve dans les fonctions ci-dessous.
- getDownloadUrl
node_modules/prebuild-install/util.js
Génère l’url vers le binaire correspondant dans les artefacts de release sur github. L’url générée prend on considération l’environnement d’exécution.
L’url ressemblera à https://api.github.com/repose/WiseLibs/better-sqlite3/releases/{name}-v{version}-{runtime}-v{abi}-{platform}{libc}-{arch}.tar.gz
- downloadPrebuild
node_modules/prebuild-install/download.js
Télécharge le binaire et le décompresse si nécessaire.
On peut rapidement confirmer le téléchargement avec le script suivant.
import { arch, platform } from "os";
import { promisify } from "util";
//@ts-expect-error - no types available
import { getDownloadUrl } from "prebuild-install/util";
//@ts-expect-error - no types available
import { default as downloadPrebuild } from "prebuild-install/download";
const downloadPrebuildAsync = promisify(downloadPrebuild);
const runtime = process.versions.electron ? "electron" : "node";
* Finding the right options required
* reverse engineering 'prebuild-install' code
url: "git://github.com/WiseLibs/better-sqlite3.git",
const url = getDownloadUrl(opts);
downloadPrebuildAsync(url, opts)
log.info("Downloaded better-sqlite3");
log.error("Error downloading better-sqlite3", err);