From 25621d3a21ec06ce340592511891b577d4062476 Mon Sep 17 00:00:00 2001 From: TheNEwmanator15 Date: Thu, 24 Sep 2026 21:53:40 +0100 Subject: [PATCH] Cache field lookups when loading item data Zotero.Item#setField() in loadIn mode repeats the same lookups for every value (field ID, type-specific field, validity for the type, multiline), several of which allocate. Cache them per item type and field in Zotero.ItemFields._getLoadInfo(). The cache is rebuilt by ItemFields.init() and dropped if any method it depends on is replaced, so an overridden method is still consulted. Also look up each item type's fields once per load rather than once per item. setField() is still called for every value with the same arguments, so code that wraps it sees the same calls, and the loaded data is identical. Co-Authored-By: Claude Opus 5.5 --- chrome/content/zotero/xpcom/data/item.js | 25 ++++++++- .../content/zotero/xpcom/data/itemFields.js | 56 ++++++++++++++++++- chrome/content/zotero/xpcom/data/items.js | 9 ++- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js index c80df3c129..721b795312 100644 --- a/chrome/content/zotero/xpcom/data/item.js +++ b/chrome/content/zotero/xpcom/data/item.js @@ -794,7 +794,30 @@ Zotero.Item.prototype.setField = function (field, value, loadIn) { if (!itemTypeID) { throw new Error('Item type must be set before setting field data'); } - + + // Loading calls this for every stored value, so use cached field lookups. + // Same result as the general path below. + if (loadIn && typeof field == 'number') { + let info = Zotero.ItemFields._getLoadInfo(itemTypeID, field); + if (info && !info.isISBN) { + if (field == info.titleID && this.isNote()) { + this._noteTitle = value ? value : ""; + return true; + } + if (value !== false && !info.valid) { + Zotero.debug("'" + field + "' is not a valid field for type '" + + Zotero.ItemTypes.getName(itemTypeID) + "'" + " -- ignoring value '" + value + "'", 2); + return false; + } + if (typeof value == 'string' && !info.multiline + && (value.includes('\n') || value.includes('\r'))) { + value = value.replace(/[\r\n]+/g, " "); + } + this._itemData[info.fieldID] = value; + return true; + } + } + var fieldID = Zotero.ItemFields.getID(field); if (!fieldID) { throw new Error('"' + field + '" is not a valid itemData field'); diff --git a/chrome/content/zotero/xpcom/data/itemFields.js b/chrome/content/zotero/xpcom/data/itemFields.js index a9e16e1675..a884fe6dbe 100644 --- a/chrome/content/zotero/xpcom/data/itemFields.js +++ b/chrome/content/zotero/xpcom/data/itemFields.js @@ -39,7 +39,9 @@ Zotero.ItemFields = new function () { var _typeFieldNamesByBase = {}; var _baseFieldIDsByTypeAndField = {}; var _autocompleteFields = null; - + var _loadInfo = []; + var _loadInfoMethods = {}; + // Privileged methods this.getName = getName; this.getID = getID; @@ -59,6 +61,7 @@ Zotero.ItemFields = new function () { this.init = async function () { _fields = {}; _fieldsFormats = []; + _loadInfo = []; var result = await Zotero.DB.queryAsync('SELECT * FROM fieldFormats'); @@ -123,6 +126,57 @@ Zotero.ItemFields = new function () { } + /** + * The lookups Zotero.Item#setField() does for every value loaded from the + * database, cached per item type and field + * + * The cache is dropped if any method it depends on is replaced, so an + * overridden method is still consulted. + * + * @param {Integer} itemTypeID + * @param {Integer} fieldID + * @return {Object|false} - false for an unknown field + */ + this._getLoadInfo = function (itemTypeID, fieldID) { + if (_loadInfoMethods.getID !== this.getID + || _loadInfoMethods.getFieldIDFromTypeAndBase !== this.getFieldIDFromTypeAndBase + || _loadInfoMethods.isValidForType !== this.isValidForType + || _loadInfoMethods.isMultiline !== this.isMultiline + || _loadInfoMethods.getItemTypeID !== Zotero.ItemTypes.getID) { + _loadInfo = []; + _loadInfoMethods = { + getID: this.getID, + getFieldIDFromTypeAndBase: this.getFieldIDFromTypeAndBase, + isValidForType: this.isValidForType, + isMultiline: this.isMultiline, + getItemTypeID: Zotero.ItemTypes.getID + }; + } + var byField = _loadInfo[itemTypeID]; + if (!byField) { + byField = _loadInfo[itemTypeID] = new Map(); + } + var info = byField.get(fieldID); + if (info === undefined) { + if (!this.getID(fieldID)) { + info = false; + } + else { + let resolvedID = this.getFieldIDFromTypeAndBase(itemTypeID, fieldID) || fieldID; + info = { + fieldID: resolvedID, + titleID: this.getID('title'), + valid: this.isValidForType(resolvedID, itemTypeID), + multiline: this.isMultiline(resolvedID), + isISBN: resolvedID == this.getID('ISBN') + }; + } + byField.set(fieldID, info); + } + return info; + }; + + /* * Return the fieldName for a passed fieldID or fieldName */ diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js index dd35b89a53..f98b5eadcc 100644 --- a/chrome/content/zotero/xpcom/data/items.js +++ b/chrome/content/zotero/xpcom/data/items.js @@ -298,6 +298,7 @@ Zotero.Items = function () { var sql = "SELECT itemID FROM items WHERE libraryID=?" + idSQL; var params = [libraryID]; var allItemIDs = []; + var fieldIDsByType = new Map(); await Zotero.DB.queryAsync( sql, params, @@ -306,9 +307,13 @@ Zotero.Items = function () { onRow: function (row) { let itemID = row.getResultByIndex(0); let item = this._objectCache[itemID]; - + // Set nonexistent fields in the cache list to false (instead of null) - let fieldIDs = Zotero.ItemFields.getItemTypeFields(item.itemTypeID); + let fieldIDs = fieldIDsByType.get(item.itemTypeID); + if (!fieldIDs) { + fieldIDs = Zotero.ItemFields.getItemTypeFields(item.itemTypeID); + fieldIDsByType.set(item.itemTypeID, fieldIDs); + } for (let j=0; j