Wekan
Open source kanban board application built with Meteor
Alternative to: trello
v10.52
2026-07-30v10.52 2026-07-30 WeKan ® release
This release fixes the following bugs:
Fix WeKan not starting in Yandex Browser: Meteor's own transpile asked for a module that was not there. Thanks to zubzhaaaw and xet7.
Three attempts at this had added imports to client/lib/swcHelpers.js, and
10.51 still failed in Yandex Browser with the same line:
Uncaught Error: Cannot find module
'@swc/helpers/_/_possible_constructor_return'
at a.s [as link] (…) at client-rspack.js (…:285:985)
Read the stack outward: client-meteor.js links client-rspack.js, and the
failing link() is inside client-rspack.js. But the bundle rspack writes
contains no @swc/helpers specifier at all — the built
_build/main-prod/client-rspack.js has the helper bodies inlined and the string
nowhere. The import is in the copy of that file that Meteor compiled, and
Meteor put it there: package.json says "meteor": { "modern": true }, which
turns on Meteor 3.3+‘s SWC transpiler for every file; babel-compiler sets
jsc.externalHelpers: true whenever node_modules/@swc/helpers exists — and
WeKan depends on it — so the output imports its helpers instead of inlining
them; and for web.browser.legacy that transpile passes no jsc.target and an
env.targets down to IE 11, so class is lowered to ES5 and the output gains
import { _ } from "@swc/helpers/_/_possible_constructor_return". That
specifier is not in the legacy bundle’s module tree, so the whole app fails
before anything runs. The modern bundle was never affected: nothing there is
lowered that far, and where a helper is needed the modern module runtime
resolves module (the esm/ file) while the legacy one prefers main
(../../cjs/_x.cjs).
That is also why the earlier fixes could not work, whatever they did.
client/lib/swcHelpers.js is in the rspack graph; rspack resolves its
imports and inlines the helper bodies, so they never become entries in Meteor’s
module tree — which is the tree the failing link() searches. No import written
in app code can satisfy a link that Meteor’s transpiler adds afterwards.
So /.swcrc sets jsc.externalHelpers: false, and SWC inlines each helper into
the file that needs it: the import is never emitted, so it cannot go missing.
Both readers of that file merge it over their own defaults and neither preserves
this key — Meteor keeps only jsc.target, env.targets and module.type, and
@meteorjs/rspack only jsc.target — so it reaches the Meteor transpile and
the rspack build alike. It costs one copy of a helper per file that uses one,
which is the right trade for a bundle that otherwise does not load.
.gitignore’s *.sw* is there for vim’s .swp/.swo and matched .swcrc
too, so adding the file was silently a no-op; !.swcrc now follows it, and the
test fails if that exception is removed or moved above the rule it corrects.
Only a browser served the legacy bundle could hit any of this, which is why it
read as a Yandex bug. useragent-ng reports Yandex Browser as the family
“Yandex Browser”, webapp camel-cases that to yandexBrowser, and
modern-browsers has neither a minimum nor a chrome alias for that name —
isModern() is false for a name nobody declared — so every Yandex Browser user
was served ES5. server/modernBrowsers.js declares yandexBrowser: 18: its
major version is a year, and 18.x is already Chromium 64, past full ES2015 (51)
and dynamic import() (63). Samsung Internet, Vivaldi, Opera Mobile, Whale,
MIUI, UC Browser and QQ Browser are in the same position and are left alone on
purpose — their version numbers do not map onto a Chromium version — and the
file says so rather than looking as though they were forgotten.
The test reproduces Meteor’s decision — webapp’s camelCase, modern-browsers’
alias table and lookup — against real user agent strings, so it fails if the key
stops matching the family name or the minimum stops covering the versions in the
reports.
A write the database was too busy to take is now retried, which nothing had ever done. Thanks to Nissulya and xet7.
The restart loop is gone — a transient database error no longer ends the boot or the process — and the reporter’s 10.49 log shows the same contention arriving in front of a user instead:
Exception while invoking method '/users/updateAsync' MongoServerError:
… [collection.go:191 sqlite.(*collection).UpdateAll]
[db.go:151 fsql.(*DB).InTransaction] database is locked (5) (SQLITE_BUSY)
That error goes to the client: the edit fails, and the user reloads to find
out what happened — which is the complaint that they only see their cards after
a reload. And it was not supposed to. models/lib/databaseErrors.js has
classified a locked SQLite as { id: 'deadlock', act: 'retry' } with “Retried
automatically.” since it was written, and server/00processErrors.js says “the
write is retried by whatever issued it”. Nothing read act. No code in WeKan
retried anything, anywhere.
SQLITE_BUSY is the one database error a client is meant to handle itself.
SQLite has a single writer; a second writer that arrives while the lock is held
is told to come back, and nothing was applied — FerretDB takes the lock before
the transaction, so a BUSY answer means the write did not happen. Retrying the
same write is the documented behaviour and stays idempotent: Meteor has already
chosen the _id, so a retry that raced a duplicate is refused by the unique
index rather than inserted twice.
server/00retryBusyWrites.js wraps insertAsync, updateAsync, upsertAsync
and removeAsync: a transient database error is retried with exponential
backoff and full jitter, bounded at five attempts and two seconds in total
(WEKAN_DB_RETRY_*). Bounded on purpose — retrying for longer holds a method
invocation and its slot in the connection pool while the contention it is
waiting for gets worse. When it still fails, the original error is thrown
unchanged, so a database that is genuinely stuck looks exactly as it did before
and nothing is hidden; anything that is not transient is rethrown on the first
attempt, so a full disk is not waited on four times before being reported.
It is loaded from server/imports.js, after the Meteor packages, so it sits
outside the wrappers collection2 and collection-hooks put on the same prototype
— which is how it sees the error at all, because by then the busy error is
collection2’s ValidationError with the database’s message inside it, the form
in the reporter’s SyncedCron traces.
Writes only: every error in the reports is a write, which is what contends for
the one writer, and SQLite in WAL mode does not block a reader against a writer.
Retries are counted and summarised at most once a minute, so a contended
database is visible without the log becoming the new problem, and a write
finally given up on is recorded for Admin Panel / Problems — except an
eventlog write, which is how the recorder writes, and reporting its failure
would call the recorder from inside itself.
The reporter asked outright whether PostgreSQL is an option for a snap
installation. It is, and it has been: the advice for constant contention now
says how — snap set wekan wekan-ferretdb-handler=postgresql wekan-ferretdb-url=…, or docker-compose-ferretdb-v1-postgresql.yml in Docker
— instead of only saying that SQLite has one writer.
The test loads the real module against a stub collection prototype and exercises
the behaviour rather than reading the source: a busy write is retried and
succeeds with its arguments and its collection unchanged, the ValidationError
form is recognised too, a full disk is thrown at once and never retried, an
endlessly busy database gets the original error after a bounded number of
attempts and a bounded wall-clock time, the backoff grows and is jittered and
capped on the shipped numbers, reads are left unwrapped, wrapping twice is a
no-op, and the eventlog write is retried without reporting on itself.
Database problems now shows which database said it and what it said. Thanks to xet7.
A database problem arrived in Admin Panel / Problems / Database problems with an empty Category, Name and Action and a detail that said “The database returned an error WeKan has no rule for. Read the message below” — with no message below, or anywhere else on the page. The row said that something had happened and nothing about what.
Nothing was wrong with the classifier or the recorder. EventLog has a
SimpleSchema attached, and collection2 cleans every insert against it with
filter: true, so a field the schema does not declare is dropped silently on
the way to the database. The four fields the database stream uses — type
(the rule id), db (which database said it), kind and message — were never
added to that schema, so they were the four that were thrown away, and they are
exactly the four the page shows in Category, Name, Action and the message it
told the admin to read. The other four event streams were unaffected: they only
ever write fields the schema declares.
The schema declares them now, and the test pins the general rule rather than
this one instance — for each of the five event loggers, every key of the
document it inserts must be declared in the EventLog schema — so the next
field added to a logger cannot vanish the same way.
The message is shown as well: the Detail cell carries WeKan’s reading of the
error and then the database’s own sentence, which is the one thing an admin can
search for or paste into an issue. The stream search looks at message, db,
kind and type, so searching for “postgresql” or for a phrase out of the
message finds the row that displays it, and the unclassified advice no longer
promises a message “below” — it follows in the same cell.
Because that message is now stored and displayed, it loses its credentials
first. A database that refuses a login or cannot be reached quotes the
connection URL back, password and all, so classifyDatabaseError redacts the
userinfo of any URL in the message before returning it — keeping the host, the
port and the database name, which are what makes the message useful — and it is
done there rather than in the recorder so no future caller can forget it. The
message is also sanitized like every other stream’s detail: one line, control
characters out, capped.