Merge pull request #2542 from SillyTavern/wi-slash-commands-performance-improvements World Info: slash commands performance improvements

256f0a58db755de66f10f37ae9dd13874d538e96

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
3 files changed, +168 -79Ignore whitespace
public/scripts/util/StructuredCloneMap.js+56 -0
@@ -0,0 +1,56 @@
1+/**
2+ * A specialized Map class that provides consistent data storage by performing deep cloning of values.
3+ *
4+ * @template K, V
5+ * @extends Map<K, V>
6+ */
7+export class StructuredCloneMap extends Map {
8+ /**
9+ * Constructs a new StructuredCloneMap.
10+ * @param {object} options - Options for the map
11+ * @param {boolean} options.cloneOnGet - Whether to clone the value when getting it from the map
12+ * @param {boolean} options.cloneOnSet - Whether to clone the value when setting it in the map
13+ */
14+ constructor({ cloneOnGet, cloneOnSet } = { cloneOnGet: true, cloneOnSet: true }) {
15+ super();
16+ this.cloneOnGet = cloneOnGet;
17+ this.cloneOnSet = cloneOnSet;
18+ }
19+
20+ /**
21+ * Adds a new element with a specified key and value to the Map. If an element with the same key already exists, the element will be updated.
22+ *
23+ * The set value will always be a deep clone of the provided value to provide consistent data storage.
24+ *
25+ * @param {K} key - The key to set
26+ * @param {V} value - The value to set
27+ * @returns {this} The updated map
28+ */
29+ set(key, value) {
30+ if (!this.cloneOnSet) {
31+ return super.set(key, value);
32+ }
33+
34+ const clonedValue = structuredClone(value);
35+ super.set(key, clonedValue);
36+ return this;
37+ }
38+
39+ /**
40+ * Returns a specified element from the Map object.
41+ * If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
42+ *
43+ * The returned value will always be a deep clone of the cached value.
44+ *
45+ * @param {K} key - The key to get the value for
46+ * @returns {V | undefined} Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
47+ */
48+ get(key) {
49+ if (!this.cloneOnGet) {
50+ return super.get(key);
51+ }
52+
53+ const value = super.get(key);
54+ return structuredClone(value);
55+ }
56+}
public/scripts/utils.js+24 -1
@@ -271,6 +271,13 @@ export function getStringHash(str, seed = 0) {
271271}
272272
273273/**
274+ * Map of debounced functions to their timers.
275+ * Weak map is used to avoid memory leaks.
276+ * @type {WeakMap<function, any>}
277+ */
278+const debounceMap = new WeakMap();
279+
280+/**
274281 * Creates a debounced function that delays invoking func until after wait milliseconds have elapsed since the last time the debounced function was invoked.
275282 * @param {function} func The function to debounce.
276283 * @param {debounce_timeout|number} [timeout=debounce_timeout.default] The timeout based on the common enum values, or in milliseconds.
@@ -278,10 +285,26 @@ export function getStringHash(str, seed = 0) {
278285 */
279286export function debounce(func, timeout = debounce_timeout.standard) {
280287 let timer;
281288 returnlet fn = (...args) => {
282289 clearTimeout(timer);
283290 timer = setTimeout(() => { func.apply(this, args); }, timeout);
291+ debounceMap.set(func, timer);
292+ debounceMap.set(fn, timer);
284293 };
294+
295+ return fn;
296+}
297+
298+/**
299+ * Cancels a scheduled debounced function.
300+ * Does nothing if the function is not debounced or not scheduled.
301+ * @param {function} func The function to cancel. Either the original or the debounced function.
302+ */
303+export function cancelDebounce(func) {
304+ if (debounceMap.has(func)) {
305+ clearTimeout(debounceMap.get(func));
306+ debounceMap.delete(func);
307+ }
285308}
286309
287310/**
public/scripts/world-info.js+88 -78
@@ -1,5 +1,5 @@
11import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
22import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getSelect2OptionId, dynamicSelect2DataViaAjax, highlightRegex, select2ChoiceClickSubscribe, isFalseBoolean, getSanitizedFilename, checkOverwriteExistingData, getStringHash, parseStringArray, cancelDebounce } from './utils.js';
33import { extension_settings, getContext } from './extensions.js';
44import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
55import { isMobile } from './RossAscends-mods.js';
@@ -16,6 +16,7 @@ import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandE
1616import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js';
1717import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
1818import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
19+import { StructuredCloneMap } from './util/StructuredCloneMap.js';
1920
2021export {
2122 world_info,
@@ -746,7 +747,8 @@ export const wi_anchor_position = {
746747 after: 1,
747748};
748749
749-const worldInfoCache = new Map();
750+/** @type {StructuredCloneMap<string,object>} */
751+const worldInfoCache = new StructuredCloneMap({ cloneOnGet: true, cloneOnSet: false });
750752
751753/**
752754 * Gets the world info based on chat messages.
@@ -885,9 +887,15 @@ function setWorldInfoSettings(settings, data) {
885887}
886888
887889function registerWorldInfoSlashCommands() {
888- function reloadEditor(file) {
890+ /**
891+ * Reloads the editor with the specified world info file
892+ * @param {string} file - The file to load in the editor
893+ * @param {boolean} [loadIfNotSelected=false] - Indicates whether to load the file even if it's not currently selected
894+ */
895+ function reloadEditor(file, loadIfNotSelected = false) {
896+ const currentIndex = $('#world_editor_select').val();
889897 const selectedIndex = world_names.indexOf(file);
890898 if (selectedIndex !== -1 && (loadIfNotSelected || currentIndex === selectedIndex)) {
891899 $('#world_editor_select').val(selectedIndex).trigger('change');
892900 }
893901 }
@@ -1049,7 +1057,7 @@ function registerWorldInfoSlashCommands() {
10491057 entry.content = content;
10501058 }
10511059
10521060 await saveWorldInfo(file, data, true);
10531061 reloadEditor(file);
10541062
10551063 return String(entry.uid);
@@ -1100,7 +1108,7 @@ function registerWorldInfoSlashCommands() {
11001108 setOriginalDataValue(data, uid, originalDataKeyMap[field], entry[field]);
11011109 }
11021110
11031111 await saveWorldInfo(file, data, true);
11041112 reloadEditor(file);
11051113 return '';
11061114 }
@@ -1840,7 +1848,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
18401848 nextText: '>',
18411849 formatNavigator: PAGINATION_TEMPLATE,
18421850 showNavigator: true,
18431851 callback: async function (/** @type {object[]} */ page) {
18441852 // We save costly performance by removing all events before emptying. Because we know there are no relevant event handlers reacting on removing elements
18451853 // This prevents jQuery from actually going through all registered events on the controls for each entry when removing it
18461854 worldEntriesList.find('*').off();
@@ -1926,7 +1934,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
19261934
19271935 if (counter > 0) {
19281936 toastr.info(`Backfilled ${counter} titles`);
19291937 await saveWorldInfo(name, data, true);
19301938 updateEditor(navigation_option.previous);
19311939 }
19321940 });
@@ -2026,7 +2034,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
20262034
20272035 console.table(Object.keys(data.entries).map(uid => data.entries[uid]).map(x => ({ uid: x.uid, key: x.key.join(','), displayIndex: x.displayIndex })));
20282036
20292037 await saveWorldInfo(name, data, true);
20302038 },
20312039 });
20322040 //$("#world_popup_entries_list").disableSelection();
@@ -2298,7 +2306,7 @@ function getWorldEntry(name, data, entry) {
22982306 templateResult: item => templateStyling(item, { searchStyle: true }),
22992307 templateSelection: item => templateStyling(item),
23002308 });
23012309 input.on('change', async function (_, { skipReset, noSave } = {}) {
23022310 const uid = $(this).data('uid');
23032311 /** @type {string[]} */
23042312 const keys = ($(this).select2('data')).map(x => x.text);
@@ -2307,7 +2315,7 @@ function getWorldEntry(name, data, entry) {
23072315 if (!noSave) {
23082316 data.entries[uid][entryPropName] = keys;
23092317 setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
23102318 await saveWorldInfo(name, data);
23112319 }
23122320 });
23132321 input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
@@ -2338,14 +2346,14 @@ function getWorldEntry(name, data, entry) {
23382346 template.find(`select[name="${entryPropName}"]`).hide();
23392347 input.show();
23402348
23412349 input.on('input', async function (_, { skipReset, noSave } = {}) {
23422350 const uid = $(this).data('uid');
23432351 const value = String($(this).val());
23442352 !skipReset && resetScrollHeight(this);
23452353 if (!noSave) {
23462354 data.entries[uid][entryPropName] = splitKeywordsAndRegexes(value);
23472355 setOriginalDataValue(data, uid, originalDataValueName, data.entries[uid][entryPropName]);
23482356 await saveWorldInfo(name, data);
23492357 }
23502358 });
23512359 input.val(entry[entryPropName].join(', ')).trigger('input', { skipReset: true });
@@ -2384,12 +2392,12 @@ function getWorldEntry(name, data, entry) {
23842392 event.stopPropagation();
23852393 });
23862394
23872395 selectiveLogicDropdown.on('input', async function () {
23882396 const uid = $(this).data('uid');
23892397 const value = Number($(this).val());
23902398 data.entries[uid].selectiveLogic = !isNaN(value) ? value : world_info_logic.AND_ANY;
23912399 setOriginalDataValue(data, uid, 'selectiveLogic', data.entries[uid].selectiveLogic);
23922400 await saveWorldInfo(name, data);
23932401 });
23942402
23952403 template
@@ -2404,7 +2412,7 @@ function getWorldEntry(name, data, entry) {
24042412 // exclude characters checkbox
24052413 const characterExclusionInput = template.find('input[name="character_exclusion"]');
24062414 characterExclusionInput.data('uid', entry.uid);
24072415 characterExclusionInput.on('input', async function () {
24082416 const uid = $(this).data('uid');
24092417 const value = $(this).prop('checked');
24102418 characterFilterLabel.text(value ? 'Exclude Character(s)' : 'Filter to Character(s)');
@@ -2438,7 +2446,7 @@ function getWorldEntry(name, data, entry) {
24382446 }
24392447
24402448 setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
24412449 await saveWorldInfo(name, data);
24422450 });
24432451 characterExclusionInput.prop('checked', entry.characterFilter?.isExclude ?? false).trigger('input');
24442452
@@ -2500,24 +2508,24 @@ function getWorldEntry(name, data, entry) {
25002508 );
25012509 }
25022510 setOriginalDataValue(data, uid, 'character_filter', data.entries[uid].characterFilter);
25032511 await saveWorldInfo(name, data);
25042512 });
25052513
25062514 // comment
25072515 const commentInput = template.find('textarea[name="comment"]');
25082516 const commentToggle = template.find('input[name="addMemo"]');
25092517 commentInput.data('uid', entry.uid);
25102518 commentInput.on('input', async function (_, { skipReset } = {}) {
25112519 const uid = $(this).data('uid');
25122520 const value = $(this).val();
25132521 !skipReset && resetScrollHeight(this);
25142522 data.entries[uid].comment = value;
25152523
25162524 setOriginalDataValue(data, uid, 'comment', data.entries[uid].comment);
25172525 await saveWorldInfo(name, data);
25182526 });
25192527 commentToggle.data('uid', entry.uid);
25202528 commentToggle.on('input', async function () {
25212529 const uid = $(this).data('uid');
25222530 const value = $(this).prop('checked');
25232531 //console.log(value)
@@ -2525,7 +2533,7 @@ function getWorldEntry(name, data, entry) {
25252533 .closest('.world_entry')
25262534 .find('.commentContainer');
25272535 data.entries[uid].addMemo = value;
25282536 await saveWorldInfo(name, data);
25292537 value ? commentContainer.show() : commentContainer.hide();
25302538 });
25312539
@@ -2543,13 +2551,13 @@ function getWorldEntry(name, data, entry) {
25432551
25442552 const contentInput = template.find('textarea[name="content"]');
25452553 contentInput.data('uid', entry.uid);
25462554 contentInput.on('input', async function (_, { skipCount } = {}) {
25472555 const uid = $(this).data('uid');
25482556 const value = $(this).val();
25492557 data.entries[uid].content = value;
25502558
25512559 setOriginalDataValue(data, uid, 'content', data.entries[uid].content);
25522560 await saveWorldInfo(name, data);
25532561
25542562 if (skipCount) {
25552563 return;
@@ -2573,13 +2581,13 @@ function getWorldEntry(name, data, entry) {
25732581 // selective
25742582 const selectiveInput = template.find('input[name="selective"]');
25752583 selectiveInput.data('uid', entry.uid);
25762584 selectiveInput.on('input', async function () {
25772585 const uid = $(this).data('uid');
25782586 const value = $(this).prop('checked');
25792587 data.entries[uid].selective = value;
25802588
25812589 setOriginalDataValue(data, uid, 'selective', data.entries[uid].selective);
25822590 await saveWorldInfo(name, data);
25832591
25842592 const keysecondary = $(this)
25852593 .closest('.world_entry')
@@ -2608,12 +2616,12 @@ function getWorldEntry(name, data, entry) {
26082616 /*
26092617 const constantInput = template.find('input[name="constant"]');
26102618 constantInput.data("uid", entry.uid);
26112619 constantInput.on("input", async function () {
26122620 const uid = $(this).data("uid");
26132621 const value = $(this).prop("checked");
26142622 data.entries[uid].constant = value;
26152623 setOriginalDataValue(data, uid, "constant", data.entries[uid].constant);
26162624 await saveWorldInfo(name, data);
26172625 });
26182626 constantInput.prop("checked", entry.constant).trigger("input");
26192627 */
@@ -2621,14 +2629,14 @@ function getWorldEntry(name, data, entry) {
26212629 // order
26222630 const orderInput = template.find('input[name="order"]');
26232631 orderInput.data('uid', entry.uid);
26242632 orderInput.on('input', async function () {
26252633 const uid = $(this).data('uid');
26262634 const value = Number($(this).val());
26272635
26282636 data.entries[uid].order = !isNaN(value) ? value : 0;
26292637 updatePosOrdDisplay(uid);
26302638 setOriginalDataValue(data, uid, 'insertion_order', data.entries[uid].order);
26312639 await saveWorldInfo(name, data);
26322640 });
26332641 orderInput.val(entry.order).trigger('input');
26342642 orderInput.css('width', 'calc(3em + 15px)');
@@ -2636,13 +2644,13 @@ function getWorldEntry(name, data, entry) {
26362644 // group
26372645 const groupInput = template.find('input[name="group"]');
26382646 groupInput.data('uid', entry.uid);
26392647 groupInput.on('input', async function () {
26402648 const uid = $(this).data('uid');
26412649 const value = String($(this).val()).trim();
26422650
26432651 data.entries[uid].group = value;
26442652 setOriginalDataValue(data, uid, 'extensions.group', data.entries[uid].group);
26452653 await saveWorldInfo(name, data);
26462654 });
26472655 groupInput.val(entry.group ?? '').trigger('input');
26482656 setTimeout(() => createEntryInputAutocomplete(groupInput, getInclusionGroupCallback(data), { allowMultiple: true }), 1);
@@ -2650,19 +2658,19 @@ function getWorldEntry(name, data, entry) {
26502658 // inclusion priority
26512659 const groupOverrideInput = template.find('input[name="groupOverride"]');
26522660 groupOverrideInput.data('uid', entry.uid);
26532661 groupOverrideInput.on('input', async function () {
26542662 const uid = $(this).data('uid');
26552663 const value = $(this).prop('checked');
26562664 data.entries[uid].groupOverride = value;
26572665 setOriginalDataValue(data, uid, 'extensions.group_override', data.entries[uid].groupOverride);
26582666 await saveWorldInfo(name, data);
26592667 });
26602668 groupOverrideInput.prop('checked', entry.groupOverride).trigger('input');
26612669
26622670 // group weight
26632671 const groupWeightInput = template.find('input[name="groupWeight"]');
26642672 groupWeightInput.data('uid', entry.uid);
26652673 groupWeightInput.on('input', async function () {
26662674 const uid = $(this).data('uid');
26672675 let value = Number($(this).val());
26682676 const min = Number($(this).attr('min'));
@@ -2679,46 +2687,46 @@ function getWorldEntry(name, data, entry) {
26792687
26802688 data.entries[uid].groupWeight = !isNaN(value) ? Math.abs(value) : 1;
26812689 setOriginalDataValue(data, uid, 'extensions.group_weight', data.entries[uid].groupWeight);
26822690 await saveWorldInfo(name, data);
26832691 });
26842692 groupWeightInput.val(entry.groupWeight ?? DEFAULT_WEIGHT).trigger('input');
26852693
26862694 // sticky
26872695 const sticky = template.find('input[name="sticky"]');
26882696 sticky.data('uid', entry.uid);
26892697 sticky.on('input', async function () {
26902698 const uid = $(this).data('uid');
26912699 const value = Number($(this).val());
26922700 data.entries[uid].sticky = !isNaN(value) ? value : null;
26932701
26942702 setOriginalDataValue(data, uid, 'extensions.sticky', data.entries[uid].sticky);
26952703 await saveWorldInfo(name, data);
26962704 });
26972705 sticky.val(entry.sticky > 0 ? entry.sticky : '').trigger('input');
26982706
26992707 // cooldown
27002708 const cooldown = template.find('input[name="cooldown"]');
27012709 cooldown.data('uid', entry.uid);
27022710 cooldown.on('input', async function () {
27032711 const uid = $(this).data('uid');
27042712 const value = Number($(this).val());
27052713 data.entries[uid].cooldown = !isNaN(value) ? value : null;
27062714
27072715 setOriginalDataValue(data, uid, 'extensions.cooldown', data.entries[uid].cooldown);
27082716 await saveWorldInfo(name, data);
27092717 });
27102718 cooldown.val(entry.cooldown > 0 ? entry.cooldown : '').trigger('input');
27112719
27122720 // delay
27132721 const delay = template.find('input[name="delay"]');
27142722 delay.data('uid', entry.uid);
27152723 delay.on('input', async function () {
27162724 const uid = $(this).data('uid');
27172725 const value = Number($(this).val());
27182726 data.entries[uid].delay = !isNaN(value) ? value : null;
27192727
27202728 setOriginalDataValue(data, uid, 'extensions.delay', data.entries[uid].delay);
27212729 await saveWorldInfo(name, data);
27222730 });
27232731 delay.val(entry.delay > 0 ? entry.delay : '').trigger('input');
27242732
@@ -2731,14 +2739,14 @@ function getWorldEntry(name, data, entry) {
27312739 const depthInput = template.find('input[name="depth"]');
27322740 depthInput.data('uid', entry.uid);
27332741
27342742 depthInput.on('input', async function () {
27352743 const uid = $(this).data('uid');
27362744 const value = Number($(this).val());
27372745
27382746 data.entries[uid].depth = !isNaN(value) ? value : 0;
27392747 updatePosOrdDisplay(uid);
27402748 setOriginalDataValue(data, uid, 'extensions.depth', data.entries[uid].depth);
27412749 await saveWorldInfo(name, data);
27422750 });
27432751 depthInput.val(entry.depth ?? DEFAULT_DEPTH).trigger('input');
27442752 depthInput.css('width', 'calc(3em + 15px)');
@@ -2750,7 +2758,7 @@ function getWorldEntry(name, data, entry) {
27502758
27512759 const probabilityInput = template.find('input[name="probability"]');
27522760 probabilityInput.data('uid', entry.uid);
27532761 probabilityInput.on('input', async function () {
27542762 const uid = $(this).data('uid');
27552763 const value = Number($(this).val());
27562764
@@ -2766,7 +2774,7 @@ function getWorldEntry(name, data, entry) {
27662774 }
27672775
27682776 setOriginalDataValue(data, uid, 'extensions.probability', data.entries[uid].probability);
27692777 await saveWorldInfo(name, data);
27702778 });
27712779 probabilityInput.val(entry.probability).trigger('input');
27722780 probabilityInput.css('width', 'calc(3em + 15px)');
@@ -2778,14 +2786,14 @@ function getWorldEntry(name, data, entry) {
27782786
27792787 const probabilityToggle = template.find('input[name="useProbability"]');
27802788 probabilityToggle.data('uid', entry.uid);
27812789 probabilityToggle.on('input', async function () {
27822790 const uid = $(this).data('uid');
27832791 const value = $(this).prop('checked');
27842792 data.entries[uid].useProbability = value;
27852793 const probabilityContainer = $(this)
27862794 .closest('.world_entry')
27872795 .find('.probabilityContainer');
27882796 await saveWorldInfo(name, data);
27892797 value ? probabilityContainer.show() : probabilityContainer.hide();
27902798
27912799 if (value && data.entries[uid].probability === null) {
@@ -2814,7 +2822,7 @@ function getWorldEntry(name, data, entry) {
28142822 // Prevent closing the drawer on clicking the input
28152823 event.stopPropagation();
28162824 });
28172825 positionInput.on('input', async function () {
28182826 const uid = $(this).data('uid');
28192827 const value = Number($(this).val());
28202828 data.entries[uid].position = !isNaN(value) ? value : 0;
@@ -2836,7 +2844,7 @@ function getWorldEntry(name, data, entry) {
28362844 // Write the original value as extensions field
28372845 setOriginalDataValue(data, uid, 'extensions.position', data.entries[uid].position);
28382846 setOriginalDataValue(data, uid, 'extensions.role', data.entries[uid].role);
28392847 await saveWorldInfo(name, data);
28402848 });
28412849
28422850 const roleValue = entry.position === world_info_position.atDepth ? String(entry.role ?? extension_prompt_roles.SYSTEM) : '';
@@ -2852,12 +2860,12 @@ function getWorldEntry(name, data, entry) {
28522860 /*
28532861 const disableInput = template.find('input[name="disable"]');
28542862 disableInput.data("uid", entry.uid);
28552863 disableInput.on("input", async function () {
28562864 const uid = $(this).data("uid");
28572865 const value = $(this).prop("checked");
28582866 data.entries[uid].disable = value;
28592867 setOriginalDataValue(data, uid, "enabled", !data.entries[uid].disable);
28602868 await saveWorldInfo(name, data);
28612869 });
28622870 disableInput.prop("checked", entry.disable).trigger("input");
28632871 */
@@ -2869,7 +2877,7 @@ function getWorldEntry(name, data, entry) {
28692877 // Prevent closing the drawer on clicking the input
28702878 event.stopPropagation();
28712879 });
28722880 entryStateSelector.on('input', async function () {
28732881 const uid = entry.uid;
28742882 const value = $(this).val();
28752883 switch (value) {
@@ -2910,7 +2918,7 @@ function getWorldEntry(name, data, entry) {
29102918 template.addClass('disabledWIEntry');
29112919 break;
29122920 }
29132921 await saveWorldInfo(name, data);
29142922
29152923 });
29162924
@@ -2930,52 +2938,50 @@ function getWorldEntry(name, data, entry) {
29302938 .prop('selected', true)
29312939 .trigger('input');
29322940
2933- saveWorldInfo(name, data);
2934-
29352941 // exclude recursion
29362942 const excludeRecursionInput = template.find('input[name="exclude_recursion"]');
29372943 excludeRecursionInput.data('uid', entry.uid);
29382944 excludeRecursionInput.on('input', async function () {
29392945 const uid = $(this).data('uid');
29402946 const value = $(this).prop('checked');
29412947 data.entries[uid].excludeRecursion = value;
29422948 setOriginalDataValue(data, uid, 'extensions.exclude_recursion', data.entries[uid].excludeRecursion);
29432949 await saveWorldInfo(name, data);
29442950 });
29452951 excludeRecursionInput.prop('checked', entry.excludeRecursion).trigger('input');
29462952
29472953 // prevent recursion
29482954 const preventRecursionInput = template.find('input[name="prevent_recursion"]');
29492955 preventRecursionInput.data('uid', entry.uid);
29502956 preventRecursionInput.on('input', async function () {
29512957 const uid = $(this).data('uid');
29522958 const value = $(this).prop('checked');
29532959 data.entries[uid].preventRecursion = value;
29542960 setOriginalDataValue(data, uid, 'extensions.prevent_recursion', data.entries[uid].preventRecursion);
29552961 await saveWorldInfo(name, data);
29562962 });
29572963 preventRecursionInput.prop('checked', entry.preventRecursion).trigger('input');
29582964
29592965 // delay until recursion
29602966 const delayUntilRecursionInput = template.find('input[name="delay_until_recursion"]');
29612967 delayUntilRecursionInput.data('uid', entry.uid);
29622968 delayUntilRecursionInput.on('input', async function () {
29632969 const uid = $(this).data('uid');
29642970 const value = $(this).prop('checked');
29652971 data.entries[uid].delayUntilRecursion = value;
29662972 setOriginalDataValue(data, uid, 'extensions.delay_until_recursion', data.entries[uid].delayUntilRecursion);
29672973 await saveWorldInfo(name, data);
29682974 });
29692975 delayUntilRecursionInput.prop('checked', entry.delayUntilRecursion).trigger('input');
29702976
29712977 // duplicate button
29722978 const duplicateButton = template.find('.duplicate_entry_button');
29732979 duplicateButton.data('uid', entry.uid);
29742980 duplicateButton.on('click', async function () {
29752981 const uid = $(this).data('uid');
29762982 const entry = duplicateWorldInfoEntry(data, uid);
29772983 if (entry) {
29782984 await saveWorldInfo(name, data);
29792985 updateEditor(entry.uid);
29802986 }
29812987 });
@@ -2983,18 +2989,18 @@ function getWorldEntry(name, data, entry) {
29832989 // delete button
29842990 const deleteButton = template.find('.delete_entry_button');
29852991 deleteButton.data('uid', entry.uid);
29862992 deleteButton.on('click', async function () {
29872993 const uid = $(this).data('uid');
29882994 deleteWorldInfoEntry(data, uid);
29892995 deleteOriginalDataValue(data, uid);
29902996 await saveWorldInfo(name, data);
29912997 updateEditor(navigation_option.previous);
29922998 });
29932999
29943000 // scan depth
29953001 const scanDepthInput = template.find('input[name="scanDepth"]');
29963002 scanDepthInput.data('uid', entry.uid);
29973003 scanDepthInput.on('input', async function () {
29983004 const uid = $(this).data('uid');
29993005 const isEmpty = $(this).val() === '';
30003006 const value = Number($(this).val());
@@ -3014,59 +3020,59 @@ function getWorldEntry(name, data, entry) {
30143020
30153021 data.entries[uid].scanDepth = !isEmpty && !isNaN(value) && value >= 0 && value <= MAX_SCAN_DEPTH ? Math.floor(value) : null;
30163022 setOriginalDataValue(data, uid, 'extensions.scan_depth', data.entries[uid].scanDepth);
30173023 await saveWorldInfo(name, data);
30183024 });
30193025 scanDepthInput.val(entry.scanDepth ?? null).trigger('input');
30203026
30213027 // case sensitive select
30223028 const caseSensitiveSelect = template.find('select[name="caseSensitive"]');
30233029 caseSensitiveSelect.data('uid', entry.uid);
30243030 caseSensitiveSelect.on('input', async function () {
30253031 const uid = $(this).data('uid');
30263032 const value = $(this).val();
30273033
30283034 data.entries[uid].caseSensitive = value === 'null' ? null : value === 'true';
30293035 setOriginalDataValue(data, uid, 'extensions.case_sensitive', data.entries[uid].caseSensitive);
30303036 await saveWorldInfo(name, data);
30313037 });
30323038 caseSensitiveSelect.val((entry.caseSensitive === null || entry.caseSensitive === undefined) ? 'null' : entry.caseSensitive ? 'true' : 'false').trigger('input');
30333039
30343040 // match whole words select
30353041 const matchWholeWordsSelect = template.find('select[name="matchWholeWords"]');
30363042 matchWholeWordsSelect.data('uid', entry.uid);
30373043 matchWholeWordsSelect.on('input', async function () {
30383044 const uid = $(this).data('uid');
30393045 const value = $(this).val();
30403046
30413047 data.entries[uid].matchWholeWords = value === 'null' ? null : value === 'true';
30423048 setOriginalDataValue(data, uid, 'extensions.match_whole_words', data.entries[uid].matchWholeWords);
30433049 await saveWorldInfo(name, data);
30443050 });
30453051 matchWholeWordsSelect.val((entry.matchWholeWords === null || entry.matchWholeWords === undefined) ? 'null' : entry.matchWholeWords ? 'true' : 'false').trigger('input');
30463052
30473053 // use group scoring select
30483054 const useGroupScoringSelect = template.find('select[name="useGroupScoring"]');
30493055 useGroupScoringSelect.data('uid', entry.uid);
30503056 useGroupScoringSelect.on('input', async function () {
30513057 const uid = $(this).data('uid');
30523058 const value = $(this).val();
30533059
30543060 data.entries[uid].useGroupScoring = value === 'null' ? null : value === 'true';
30553061 setOriginalDataValue(data, uid, 'extensions.use_group_scoring', data.entries[uid].useGroupScoring);
30563062 await saveWorldInfo(name, data);
30573063 });
30583064 useGroupScoringSelect.val((entry.useGroupScoring === null || entry.useGroupScoring === undefined) ? 'null' : entry.useGroupScoring ? 'true' : 'false').trigger('input');
30593065
30603066 // automation id
30613067 const automationIdInput = template.find('input[name="automationId"]');
30623068 automationIdInput.data('uid', entry.uid);
30633069 automationIdInput.on('input', async function () {
30643070 const uid = $(this).data('uid');
30653071 const value = $(this).val();
30663072
30673073 data.entries[uid].automationId = value;
30683074 setOriginalDataValue(data, uid, 'extensions.automation_id', data.entries[uid].automationId);
30693075 await saveWorldInfo(name, data);
30703076 });
30713077 automationIdInput.val(entry.automationId ?? '').trigger('input');
30723078 setTimeout(() => createEntryInputAutocomplete(automationIdInput, getAutomationIdCallback(data)), 1);
@@ -3301,6 +3307,9 @@ function createWorldInfoEntry(_name, data) {
33013307}
33023308
33033309async function _save(name, data) {
3310+ // Prevent double saving if both immediate and debounced save are called
3311+ cancelDebounce(saveWorldDebounced);
3312+
33043313 await fetch('/api/worldinfo/edit', {
33053314 method: 'POST',
33063315 headers: getRequestHeaders(),
@@ -3309,12 +3318,13 @@ async function _save(name, data) {
33093318 eventSource.emit(event_types.WORLDINFO_UPDATED, name, data);
33103319}
33113320
33123321async function saveWorldInfo(name, data, immediately = false) {
33133322 if (!name || !data) {
33143323 return;
33153324 }
33163325
3317- worldInfoCache.delete(name);
3326+ // Update cache immediately, so any future call can pull from this
3327+ worldInfoCache.set(name, data);
33183328
33193329 if (immediately) {
33203330 return await _save(name, data);