Retour aux Actualités
Développement

Mes meilleurs bugs : Paires de substituts invalides

16 mai 2026Source

Si vous êtes dans le métier de construire des choses qui fonctionnent sur des ordinateurs à long terme, je pense que vous finirez par acquérir une histoire de bug préférée. C'est la mienne. J'ai également créé un outil interactif où vous pouvez explorer les concepts sous-jacents à ce bug. Le bug : deux émojis entrent, aucun n'en sort J'étais en train de migrer un éditeur legacy vers une expérience collaborative avec mon équipe. TipTap en haut (c'est un wrapper autour de ProseMirror), Yjs en dessous qui gère la magie du CRDT pour la synchronisation en temps réel. Cela fonctionnait bien ! Presque toujours. Dans nos jours alpha/early release, lorsque tout était encore presque interne ou des utilisateurs de lancement précoce, il arrivait parfois que l'éditeur s'arrête simplement à sauvegarder le contenu. Silencieusement. Vous continuez à écrire et tout semble bien, mais vos modifications ne se synchronisent pas avec le document Yjs. La prochaine fois que vous ouvrez la page, tout ce que vous avez écrit depuis le point de défaillance est disparu. C'était terrifiant, très rare et presque impossible à diagnostiquer car nous n'avions jamais réussi à le reproduire. Nous avons vraiment essayé ! Mes premières hypothèses se concentraient sur les connexions Wi-Fi instables et les comportements WebSocket inhabituels, mais aucune quantité de ralentissement ou d'allumage/éteignage du Wi-Fi n'a semblé reproduire l'erreur. L'expérience était surprenamment résistante dans ces cas, dans ma mémoire. Il semblait que cela se produisait au hasard, jamais lorsque quelqu'un regardait. Pas d'erreurs évidentes dans la console, pas de trace de pile, pas de crash. Seulement... "Hey, je pense que mes modifications n'ont pas été enregistrées." Puis un jour, notre gestionnaire de produit a résolu le problème. C'était pas une chose facile à trouver. Il avait vécu plus que tout le monde (probablement parce qu'il était le meilleur à tester notre produit) et avait commencé à étroiter méthodiquement. "Je me sens comme si j'étais fou, mais je pense que c'est lorsque je tape des caractères spécifiques ensemble, vais en arrière et insère un caractère entre eux..." Il avait utilisé 🟢 et 🔴 dans ses e-mails hebdomadaires de statut de projet pour communiquer la santé générale. Vert pour en ligne, rouge pour à risque. Chaque semaine, le modèle qu'il utilisait avait les deux caractères déjà présents et il les aurait simplement supprimés (Généralement le rouge, je suis heureux de dire!). Dans cette occasion, il avait copié le cercle vert et collé le rouge devant lui à un moment donné, ou peut-être à l'envers. Cette opération spécifique— insérer un emoji multi-byte adjacent à un autre— activait une division dans la bibliothèque CRDT sous-jacente, qui divisait une paire de substituts en deux. Je me souviens d'avoir été en réunion lorsque je lui ai montré cela à moi et à l'un de mes employés directs qui s'était occupé de la transition d'édition collaborative. Je dois avoir été un peu trop excité—I vis pour des bugs exotiques:""Je me sens comme si vous étiez excité par cela," a-t-il dit. Il n'avait pas tort. En plus du plaisir, pas tous les émojis activaient le problème. Seuls ceux qui se trouvaient au-dessus de U+FFFF et qui nécessitaient des paires de substituts. Et pas toutes les modifications aboutissaient au problème—seulement celles qui provoquaient une division à un offset de byte exactement incorrect. C'était un bug difficile à déboguer avant que nous ne sachions ce qui se passait. Unités de code, points de code et clusters de graphèmes Qu'est-ce qui se passait ? Qu'est-ce que signifie "au-dessus de U+FFFF" dans le dernier paragraphe ? Quels offsets de byte ? Pour comprendre ce bug, nous devons introduire trois concepts : Unités de code → Points de code → Clusters de graphèmes Unités de code sont les valeurs 16-bit brutes que JavaScript utilise pour stocker les chaînes internes (UTF-16). C'est ce que .length compte. C'est ce que .slice() et charCodeAt() opèrent sur comme bien. JavaScript opère à niveau d'unités de code par défaut Points de code sont ce que Unicode définit effectivement comme un seul caractère. Un point de code comme U+1F920 (🤠) est un caractère en vue de Unicode, mais il est trop grand pour s'adapter dans un seul 16-bit unité de code. Alors UTF-16 le divise en deux unités de code appelées une paire de substituts : un substitut haut et un substitut bas. Les caractères ASCII simples et de nombreux symboles s'adaptent dans une unité de code, donc la distinction n'a pas d'importance pour eux. Emoji, cependant ? Presque toujours deux. Clusters de graphèmes sont ce que l'homme perçoit comme un seul caractère. C'est ce que vous voyez lorsque vous regardez une chaîne dans un éditeur de texte. Ce n'est pas nécessairement ce qui est stocké en mémoire, cependant. Dans le cas de notre bug, le cluster de graphèmes était deux émojis, mais les unités de code étaient une paire de substituts. L'opération de division a divisé la paire de substituts à un offset de byte incorrect, ce qui a entraîné le bug.

Commentaires (0)

Login or Register to apply