mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-18 00:57:55 +00:00
Two issues raised on the first push: 1. **Rev advanced before send (Bug · Correctness).** The previous diff advanced sessioninfo.rev/time inside the collect loop, before any emit ran. A concurrent updatePadClients() could then see the bumped rev and skip those revisions, and if the emit threw later, the skipped revs were lost forever. The client enforces strict newRev===rev+1 and silently stops applying on mismatch — net effect was a possible pad desync under concurrent fan-outs. Fix: snapshot startRev/startTime once, claim the (startRev, headRev] range by setting sessioninfo.rev = headRev immediately (so a concurrent run skips it), build the pending list against the local startTime, then emit. If the emit throws, roll sessioninfo.rev back to startRev so the next fan-out retries. Time is only committed after a successful send. 2. **Test re-implemented the decision (Rule violation · Reliability).** The original test re-implemented the NEW_CHANGES vs NEW_CHANGES_BATCH switch locally instead of exercising the production code. Removing the production logic would have left the test green. Fix: extract the pure wire-format decision into src/node/handler/NewChangesPacker.ts (no DB / pad dependency, so the test can import it directly under vitest), and rewrite the test to assert against the exported `buildNewChangesEmits` function from that module. PadMessageHandler now calls the same function; deleting it would fail the test. 9/9 tests across new-changes-batch + prom-instruments. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
41 lines
1.5 KiB
TypeScript
41 lines
1.5 KiB
TypeScript
// Wire-format decision for NEW_CHANGES vs NEW_CHANGES_BATCH (#7756 lever 3b).
|
|
//
|
|
// Lives in its own tiny module rather than inside PadMessageHandler so the
|
|
// pure decision can be unit-tested without standing up the full pad / DB /
|
|
// socket.io stack. PadMessageHandler.updatePadClients calls this function
|
|
// once per recipient with the queued revisions for that recipient.
|
|
|
|
export type NewChangesItem = {
|
|
newRev: number;
|
|
changeset: string;
|
|
apool: unknown;
|
|
author: string;
|
|
currentTime: number;
|
|
timeDelta: number;
|
|
};
|
|
|
|
export type NewChangesEmit =
|
|
| {type: 'COLLABROOM'; data: {type: 'NEW_CHANGES'} & NewChangesItem}
|
|
| {type: 'COLLABROOM'; data: {type: 'NEW_CHANGES_BATCH'; changes: NewChangesItem[]}};
|
|
|
|
/**
|
|
* Decide what to put on the wire for one recipient.
|
|
* - No queued revisions: nothing.
|
|
* - Batching disabled, or exactly one rev: emit one NEW_CHANGES per rev
|
|
* (legacy behaviour; preserves bytes-on-wire for the steady state).
|
|
* - Batching enabled and multiple revs: emit one NEW_CHANGES_BATCH with
|
|
* the array of revisions.
|
|
*/
|
|
export const buildNewChangesEmits = (
|
|
pending: NewChangesItem[],
|
|
batchEnabled: boolean,
|
|
): NewChangesEmit[] => {
|
|
if (pending.length === 0) return [];
|
|
if (batchEnabled && pending.length > 1) {
|
|
return [{type: 'COLLABROOM', data: {type: 'NEW_CHANGES_BATCH', changes: pending}}];
|
|
}
|
|
return pending.map((change) => ({
|
|
type: 'COLLABROOM',
|
|
data: {type: 'NEW_CHANGES', ...change},
|
|
} as NewChangesEmit));
|
|
};
|