[IMP] mail: completely clean records after deletion

This will reduce memory leaks.

Side-effect of this PR:

Accessing a field after the record is deleting is no longer supported. It was
never meant to be, but some async code relied on this behavior to check the
(non-)existence of relation fields, with an implicit check on the current record
existence.

-> After this PR, the existence check of current record needs to be explicit,
before accessing any field.

Part of task-2850875

Part-of: odoo/odoo#90934
This commit is contained in:
Sébastien Theys
2022-06-02 18:58:27 +02:00
parent 83760b9f10
commit 094df7528e
4 changed files with 22 additions and 5 deletions
+13 -5
View File
@@ -138,8 +138,7 @@ export class ModelManager {
/**
* Create the messaging singleton record.
*/
const messaging = this.models['Messaging'].insert(values);
Object.assign(odoo.__DEBUG__, { messaging });
this.models['Messaging'].insert(values);
this.messagingCreatedPromise.resolve();
await this.messaging.start();
this.messagingInitializedPromise.resolve();
@@ -204,8 +203,7 @@ export class ModelManager {
* @returns {boolean}
*/
exists(model, record) {
const existingRecord = model.__records[record.localId];
return Boolean(existingRecord && existingRecord === record);
return Boolean(record.localId);
}
/**
@@ -655,7 +653,7 @@ export class ModelManager {
get: function getFromProxy(record, prop) {
if (
!model.__fieldMap[prop] &&
!['_super', 'then'].includes(prop) &&
!['_super', 'then', 'localId'].includes(prop) &&
typeof prop !== 'symbol' &&
!(prop in record)
) {
@@ -736,6 +734,9 @@ export class ModelManager {
if (field.fieldType === 'relation') {
// ensure inverses are properly unlinked
field.parseAndExecuteCommands(record, unlinkAll(), { allowWriteReadonly: true });
if (!record.exists()) {
return; // current record might have been deleted from causality
}
}
}
this._createdRecordsComputes.delete(record);
@@ -756,7 +757,10 @@ export class ModelManager {
infoList,
});
}
delete record.__values;
delete record.__listeners;
delete model.__records[record.localId];
delete record.localId;
}
/**
@@ -1215,6 +1219,10 @@ export class ModelManager {
for (const field of model.__fieldList) {
Object.defineProperty(model.prototype, field.fieldName, {
get: function getFieldValue() { // this is bound to record
if (!this.exists()) {
// Deprecated, allows reading fields on deleted records. Use exists() instead.
return undefined;
}
if (this.modelManager._listeners.size) {
if (!this.modelManager._listenersObservingLocalId.has(this.localId)) {
this.modelManager._listenersObservingLocalId.set(this.localId, new Map());
@@ -65,9 +65,11 @@ registerModel({
if (this._isDragSourceExternalFile(ev.dataTransfer)) {
if (this.chatterOwner) {
await this.chatterOwner.fileUploader.uploadFiles(ev.dataTransfer.files);
return;
}
if (this.composerViewOwner) {
await this.composerViewOwner.fileUploader.uploadFiles(ev.dataTransfer.files);
return;
}
}
},
@@ -14,10 +14,14 @@ registerModel({
name: 'Messaging',
identifyingFields: [],
lifecycleHooks: {
_created() {
odoo.__DEBUG__.messaging = this;
},
_willDelete() {
if (this.env.services['legacy_bus_service']) {
this.env.services['legacy_bus_service'].off('window_focus', null, this._handleGlobalWindowFocus);
}
delete odoo.__DEBUG__.messaging;
},
},
recordMethods: {
@@ -292,6 +292,9 @@ registerModel({
return;
}
const fetchedMessages = await this._loadMessages();
if (!this.exists()) {
return;
}
for (const threadView of this.threadViews) {
threadView.addComponentHint('messages-loaded', { fetchedMessages });
}