[FIX] web_editor: reconnect peers after when reconnecting internet

Prior to this commit, when the user experienced an internet
disconnection, after few seconds the PeerToPeer connection state would
change to "closed" and the PeerToPeer connection would be removed
permanently. Consequently, when the user reestablished their internet
connection, the peers would remain disconnected to each other.

With this commit, peers are now re-connecting to each other.

task-3488166

closes odoo/odoo#138510

X-original-commit: b9d96629df5adf37f362eb63107fa5a9a6cd3e0d
Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
Nicolas Bayet
2023-10-16 10:39:57 +00:00
parent cc0ca70428
commit ce36c49680
4 changed files with 117 additions and 16 deletions
@@ -1202,6 +1202,9 @@ export class OdooEditor extends EventTarget {
this.observerActive();
this.dispatchEvent(new Event('historyResetFromSteps'));
}
historyGetSteps() {
return this._historySteps;
}
historyGetMissingSteps({fromStepId, toStepId}) {
const fromIndex = this._historySteps.findIndex(x => x.id === fromStepId);
const toIndex = toStepId ? this._historySteps.findIndex(x => x.id === toStepId) : this._historySteps.length;
@@ -41,7 +41,11 @@ const baseNotificationMethods = {
},
ptp_join: async function (notification) {
this._createClient(notification.fromClientId);
const clientId = notification.fromClientId;
if (this.clientsInfos[clientId] && this.clientsInfos[clientId].peerConnection) {
return this.clientsInfos[clientId];
}
this._createClient(clientId);
},
rtc_signal_icecandidate: async function (notification) {
@@ -2686,19 +2686,11 @@ export class Wysiwyg extends Component {
this._isOnline = true;
if (!this.ptp) return;
// Ask for potential missing steps from all peers.
return Promise.all(this._getPtpClients().map(client => {
return this.requestClient(
client.id,
'get_missing_steps', {
fromStepId: peek(this.odooEditor.historyGetBranchIds()).id,
},
{ transport: 'rtc' }
).then(missingSteps => {
if (missingSteps === REQUEST_ERROR) return;
this._processMissingSteps(missingSteps);
});
}));
// If it was disconnected to some peers, send the join signal again.
this.ptp.notifyAllClients('ptp_join');
// Send last step to all peers. If the peers cannot add the step, they
// will ask for missing steps.
this.ptp.notifyAllClients('oe_history_step', peek(this.odooEditor.historyGetSteps()), { transport: 'rtc' });
}
/**
* Process missing steps received from a peer.
@@ -2821,9 +2813,10 @@ export class Wysiwyg extends Component {
}
this._historySyncFinished = true;
} else {
const currentStep = this.odooEditor._historySteps[this.odooEditor._historySteps.length - 1];
// Make both send their last step to each other to
// ensure they are in sync.
this.ptp.notifyAllClients('oe_history_step', peek(this.odooEditor.historyGetSteps()), { transport: 'rtc' });
this._setCollaborativeSelection(fromClientId);
this.ptp.notifyClient(fromClientId, 'oe_history_step', currentStep, { transport: 'rtc' });
}
this.requestClient(fromClientId, 'get_client_name', undefined, { transport: 'rtc' }).then((clientName) => {
@@ -86,6 +86,16 @@ class PeerTest {
connectionClientId: this.peerId,
});
}
async removeDataChannel(peer) {
this._connections.delete(peer);
peer._connections.delete(this);
const ptpFrom = this.wysiwyg.ptp;
const ptpTo = peer.wysiwyg.ptp;
delete ptpFrom.clientsInfos[peer.peerId];
delete ptpTo.clientsInfos[this.peerId];
this.onlineMutex = new Mutex();
this._onlineResolver = undefined;
}
makeStep(fn) {
fn(this);
}
@@ -149,6 +159,8 @@ class PeerPool {
async function createPeers(peers) {
const pool = new PeerPool();
let lastGeneratedId = 0;
for (const peerId of peers) {
const iframe = document.createElement('iframe');
if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) {
@@ -263,6 +275,18 @@ async function createPeers(peers) {
_getCollaborationClientAvatarUrl() {
return '';
},
async startEdition() {
await super.startEdition(...arguments);
patch(this.odooEditor, {
_generateId() {
// Ensure the id are deterministically gererated for
// when we need to sort by them. (eg. in the
// callaboration sorting of steps)
lastGeneratedId++;
return lastGeneratedId.toString();
},
});
},
_hasICEServers() {
return true;
},
@@ -807,6 +831,83 @@ QUnit.module('web_editor', {
});
});
});
QUnit.module('Disconnect & reconnect', {}, () => {
QUnit.test('should sync history when disconnecting and reconnecting to internet', async (assert) => {
assert.expect(2);
const pool = await createPeers(['p1', 'p2']);
const peers = pool.peers;
await peers.p1.startEditor();
await peers.p2.startEditor();
await peers.p1.focus();
await peers.p2.focus();
await peers.p1.openDataChannel(peers.p2);
await peers.p1.makeStep(insert('b'));
await peers.p1.setOffline();
peers.p1.removeDataChannel(peers.p2);
const setSelection = (peer) => {
const selection = peer.document.getSelection();
const pElement = peer.wysiwyg.odooEditor.editable.querySelector('p')
const range = new Range();
range.setStart(pElement, 1);
range.setEnd(pElement, 1);
selection.removeAllRanges();
selection.addRange(range);
}
const addP = (peer, content) => {
const p = document.createElement('p');
p.textContent = content;
peer.wysiwyg.odooEditor.editable.append(p);
peer.wysiwyg.odooEditor.historyStep();
}
setSelection(peers.p1);
await peers.p1.makeStep(insert('c'));
addP(peers.p1, 'd');
setSelection(peers.p2);
await peers.p2.makeStep(insert('e'));
addP(peers.p2, 'f');
peers.p1.setOnline();
peers.p2.setOnline();
// todo: p1PromiseForMissingStep and p2PromiseForMissingStep
// should be removed when the fix of undetected missing step
// will be merged. (task-3208277)
const p1PromiseForMissingStep = new Promise((resolve) => {
patch(peers.p2.wysiwyg, {
async _processMissingSteps() {
// Wait for the p2PromiseForMissingStep to resolve
// to avoid undetected missing step.
await p2PromiseForMissingStep;
super._processMissingSteps(...arguments);
resolve();
}
})
});
const p2PromiseForMissingStep = new Promise((resolve) => {
patch(peers.p1.wysiwyg, {
async _processMissingSteps() {
super._processMissingSteps(...arguments);
resolve();
}
})
});
await peers.p1.openDataChannel(peers.p2);
await p1PromiseForMissingStep;
assert.equal(peers.p1.getValue(), `<p>ac[]eb</p><p>d</p><p>f</p>`, 'p1 should have the value merged with p2');
assert.equal(peers.p2.getValue(), `<p>ace[]b</p><p>d</p><p>f</p>`, 'p2 should have the value merged with p1');
removePeers(peers);
});
});
});
});