From 9162b155b6d2b8564ae70385b3279a5619970222 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 14:53:38 +0100 Subject: [PATCH 1/2] fix: lazy-require wordnet so the client bundle stops hanging the editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ep_define's frontend CI has timed out (6h, cancelled) on every run since the plugin started actually being installed in tests (Apr 2026), and the pad editor never loaded in CI with ep_define present — `ace_outer` was never created, no JS error was thrown, every spec (including core specs) timed out waiting for the editor. Root cause: `handleMessage.js` did `const wordnet = require('wordnet')` at module top level. Etherpad's client bundler pulls plugin hook modules into the browser bundle; evaluating wordnet's (server-only) module body in the browser silently hangs the editor bootstrap. Bisected in CI: the hook with the top-level require hangs the editor; a no-op handler (no require) loads fine; moving the require lazily inside the hook with the full plugin restored loads fine. - Require `wordnet` lazily inside `ensureInit()` so its body only runs server-side, on an actual define request. - Stop returning `[null]` for non-define messages: that told core to DROP every message (typing, cursor moves, …). Return nothing instead so only the plugin's own define messages are intercepted. Co-Authored-By: Claude Opus 4.8 (1M context) --- handleMessage.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/handleMessage.js b/handleMessage.js index e273361..286377d 100644 --- a/handleMessage.js +++ b/handleMessage.js @@ -1,9 +1,16 @@ 'use strict'; -const wordnet = require('wordnet'); - +// `wordnet` is a server-only package (it reads the WordNet database off disk). +// It MUST NOT be required at module top level: Etherpad's client bundler pulls +// plugin hook modules into the browser bundle, and evaluating wordnet's module +// body in the browser silently hangs the editor bootstrap — the ace_outer +// iframe is never created and no error is thrown, so every pad (and the whole +// frontend test suite) times out. Requiring it lazily inside the hook keeps the +// module body from ever running client-side while still working on the server. +let wordnet = null; let initPromise = null; const ensureInit = () => { + if (!wordnet) wordnet = require('wordnet'); if (!initPromise) initPromise = wordnet.init(); return initPromise; }; @@ -26,7 +33,10 @@ const sendDefinition = (context, definitions) => { exports.handleMessage = async (hookName, context) => { if (!(context && context.message && context.message.data && context.message.data.type && context.message.data.action === 'sendDefineMessage')) { - return [null]; + // Not our message — return nothing so the message keeps flowing. Returning + // `[null]` here would tell core to DROP every non-define message (typing, + // cursor moves, …), silently breaking the pad. + return; } try { await ensureInit(); From f2c41a991b804c8c31483e90e85a2a02da8a2a91 Mon Sep 17 00:00:00 2001 From: John McLear Date: Sun, 21 Jun 2026 15:01:03 +0100 Subject: [PATCH 2/2] fix: emit define reply over modern socket so the gritter shows context.client.json.send() was the socket.io v2 API and no longer exists, so the define reply never reached the client and the gritter notification never appeared (define.spec.ts:13). Emit over context.socket and use the server-resolved sessionInfo author/pad instead of client-supplied ids. --- handleMessage.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/handleMessage.js b/handleMessage.js index 286377d..07f72db 100644 --- a/handleMessage.js +++ b/handleMessage.js @@ -16,14 +16,18 @@ const ensureInit = () => { }; const sendDefinition = (context, definitions) => { - context.client.json.send({ + // `context.client.json.send(...)` was the socket.io v2 API and no longer + // exists, so the reply never reached the client (the gritter never showed). + // Emit over the modern socket, and trust the server-resolved author/pad from + // sessionInfo rather than client-supplied ids. + context.socket.emit('message', { type: 'COLLABROOM', data: { type: 'CUSTOM', payload: { action: 'recieveDefineMessage', - authorId: context.message.data.message.myAuthorId, - padId: context.message.data.message.padId, + authorId: context.sessionInfo.authorId, + padId: context.sessionInfo.padId, message: definitions, }, },