Files
wekan/server/lib/syncGuardWindow.js
Lauri Ojansivu f06ecdee09 Share guard results within one check, and make rule archive a durable Sync action.
Stored Sync stages nest - a rule archive's delivery inside the rule stage,
inside activity delivery, inside the Sync journal - and every layer's guard
checks before and after by calling the guard below it twice. One innermost
check therefore re-ran every outer guard 2^depth times, and one Sync-created
card archived by a rule took 36 s, which is why archive was held back from
durable Sync (1e40e7e06).

reuseWithinEvaluation (server/lib/syncGuardWindow.js) runs each guard at most
once per evaluation: calls made while another guard's check is running share
the results of guards already checked in it. A call from ordinary work -
between stages, after an adapter or a write - starts a new evaluation and
checks everything afresh, so a policy change or a lost lease between stages
still stops the next one. A first attempt with a 100 ms reuse window missed
exactly that and was dropped. Failures never carry over, a guard reached
again through its own check runs fresh instead of waiting on itself, and the
journal's ownership read is left as it was.

With that, archive and unarchive are registered as durable rule adapters
through the stored archive runner, and boards whose rules archive or
unarchive now take durable Sync. The full-app durable Sync test runs an
"archive every created card" rule end to end: 1.1 s for the whole test. The
rule selection test's example of an action without an adapter is now
moveCardToTop. 565 server tests pass.

Thanks to xet7 !
2026-09-30 11:50:04 +03:00

36 lines
1.7 KiB
JavaScript

'use strict';
// Stored Sync stages nest: a rule archive's delivery runs inside the rule
// stage, inside activity delivery, inside the Sync journal. Every layer's
// guard checks before and after by calling the guard below it twice, so ONE
// innermost check re-ran every outer guard - journal scope, intent, lease,
// access - 2^depth times. One rule-archived card took 36 s (measured
// 2026-09-30).
//
// reuseWithinEvaluation(check) runs each guard at most once per EVALUATION: a
// call made while another guard's check is running (in the same async
// context) shares the result of any guard already checked in it. A call made
// from ordinary work - between stages, after an adapter, after a write -
// starts a new evaluation and checks everything afresh. So a change made by
// or during work is always seen by the next check; only the redundant repeats
// inside one read-only check are removed. A failure fails the evaluation it
// happened in and is never carried into another one.
const { AsyncLocalStorage } = require('node:async_hooks');
const storage = new AsyncLocalStorage();
function reuseWithinEvaluation(check) {
if (typeof check !== 'function') throw new Error('sync-guard-evaluation-invalid');
const guard = () => {
const outer = storage.getStore();
const memo = outer ? outer.memo : new Map();
const stack = outer ? outer.stack : new Set();
// A guard reached again through its own check must not wait on itself.
if (stack.has(guard)) return Promise.resolve().then(check);
if (!memo.has(guard)) {
memo.set(guard, storage.run({ memo, stack: new Set([...stack, guard]) }, () => Promise.resolve().then(check)));
}
return memo.get(guard);
};
return guard;
}
module.exports = { reuseWithinEvaluation };