@rimori/client 2.5.42 → 2.5.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -30,6 +30,14 @@ export declare class PluginModule {
|
|
|
30
30
|
* Updated automatically when the user toggles TTS.
|
|
31
31
|
*/
|
|
32
32
|
ttsEnabled: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Single-flight cache for the plugin settings row. Holds the in-flight (or
|
|
35
|
+
* resolved) promise, not the value, so concurrent getSettings() callers join
|
|
36
|
+
* one GET instead of each issuing their own.
|
|
37
|
+
*/
|
|
38
|
+
private settingsPromise?;
|
|
39
|
+
/** Key-count migration runs at most once per plugin lifetime, not per read. */
|
|
40
|
+
private migrationDone;
|
|
33
41
|
constructor(supabase: SupabaseClient, communicationHandler: RimoriCommunicationHandler, info: RimoriInfo, ai: AIModule, eventBus: EventBusHandler);
|
|
34
42
|
/**
|
|
35
43
|
* Fetches settings based on guild configuration.
|
|
@@ -22,6 +22,14 @@ export class PluginModule {
|
|
|
22
22
|
* Updated automatically when the user toggles TTS.
|
|
23
23
|
*/
|
|
24
24
|
ttsEnabled = true;
|
|
25
|
+
/**
|
|
26
|
+
* Single-flight cache for the plugin settings row. Holds the in-flight (or
|
|
27
|
+
* resolved) promise, not the value, so concurrent getSettings() callers join
|
|
28
|
+
* one GET instead of each issuing their own.
|
|
29
|
+
*/
|
|
30
|
+
settingsPromise;
|
|
31
|
+
/** Key-count migration runs at most once per plugin lifetime, not per read. */
|
|
32
|
+
migrationDone = false;
|
|
25
33
|
constructor(supabase, communicationHandler, info, ai, eventBus) {
|
|
26
34
|
this.rimoriInfo = info;
|
|
27
35
|
this.supabase = supabase;
|
|
@@ -33,6 +41,12 @@ export class PluginModule {
|
|
|
33
41
|
this.translator = new Translator(info.interfaceLanguage, currentPlugin?.endpoint || '', ai);
|
|
34
42
|
this.ttsEnabled = info.ttsEnabled ?? true;
|
|
35
43
|
this.communicationHandler.onUpdate((updatedInfo) => {
|
|
44
|
+
// Settings are keyed by guild id — bust the cache if the guild changed so
|
|
45
|
+
// we don't serve another guild's settings from a stale promise.
|
|
46
|
+
if (updatedInfo.guild.id !== this.rimoriInfo.guild.id) {
|
|
47
|
+
this.settingsPromise = undefined;
|
|
48
|
+
this.migrationDone = false;
|
|
49
|
+
}
|
|
36
50
|
this.rimoriInfo = updatedInfo;
|
|
37
51
|
this.ttsEnabled = updatedInfo.ttsEnabled ?? true;
|
|
38
52
|
});
|
|
@@ -46,16 +60,28 @@ export class PluginModule {
|
|
|
46
60
|
* @returns The settings object or null if not found.
|
|
47
61
|
*/
|
|
48
62
|
async fetchSettings() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.
|
|
52
|
-
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
63
|
+
// Join the in-flight request if one already exists.
|
|
64
|
+
if (this.settingsPromise)
|
|
65
|
+
return this.settingsPromise;
|
|
66
|
+
const promise = (async () => {
|
|
67
|
+
const isGuildSetting = !this.rimoriInfo.guild.allowUserPluginSettings;
|
|
68
|
+
const { data } = await this.supabase
|
|
69
|
+
.schema('public')
|
|
70
|
+
.from('plugin_settings')
|
|
71
|
+
.select('*')
|
|
72
|
+
.eq('plugin_id', this.pluginId)
|
|
73
|
+
.eq('guild_id', this.rimoriInfo.guild.id)
|
|
74
|
+
.eq('is_guild_setting', isGuildSetting)
|
|
75
|
+
.maybeSingle();
|
|
76
|
+
return (data?.settings ?? null);
|
|
77
|
+
})();
|
|
78
|
+
this.settingsPromise = promise;
|
|
79
|
+
// Don't poison the cache: drop a failed fetch so the next caller retries.
|
|
80
|
+
promise.catch(() => {
|
|
81
|
+
if (this.settingsPromise === promise)
|
|
82
|
+
this.settingsPromise = undefined;
|
|
83
|
+
});
|
|
84
|
+
return promise;
|
|
59
85
|
}
|
|
60
86
|
/**
|
|
61
87
|
* Sets settings for the plugin.
|
|
@@ -65,6 +91,8 @@ export class PluginModule {
|
|
|
65
91
|
* @throws {Error} if RLS blocks the operation.
|
|
66
92
|
*/
|
|
67
93
|
async setSettings(settings) {
|
|
94
|
+
// Keep the read cache consistent with what we're about to persist.
|
|
95
|
+
this.settingsPromise = Promise.resolve(settings);
|
|
68
96
|
const isGuildSetting = !this.rimoriInfo.guild.allowUserPluginSettings;
|
|
69
97
|
const payload = {
|
|
70
98
|
plugin_id: this.pluginId,
|
|
@@ -88,6 +116,8 @@ export class PluginModule {
|
|
|
88
116
|
: updateQuery.select());
|
|
89
117
|
if (updateError) {
|
|
90
118
|
if (updateError.code === '42501' || updateError.message?.includes('policy')) {
|
|
119
|
+
// Write failed — drop the optimistic cache so reads refetch the truth.
|
|
120
|
+
this.settingsPromise = undefined;
|
|
91
121
|
throw new Error(`Cannot set ${isGuildSetting ? 'guild' : 'user'} settings: Permission denied.`);
|
|
92
122
|
}
|
|
93
123
|
// proceed to try insert in case of other issues
|
|
@@ -111,6 +141,8 @@ export class PluginModule {
|
|
|
111
141
|
if (!retryError)
|
|
112
142
|
return;
|
|
113
143
|
}
|
|
144
|
+
// Write failed — drop the optimistic cache so reads refetch the truth.
|
|
145
|
+
this.settingsPromise = undefined;
|
|
114
146
|
throw insertError;
|
|
115
147
|
}
|
|
116
148
|
}
|
|
@@ -125,14 +157,19 @@ export class PluginModule {
|
|
|
125
157
|
await this.setSettings(defaultSettings);
|
|
126
158
|
return defaultSettings;
|
|
127
159
|
}
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
160
|
+
// Key-count migration runs at most once per plugin lifetime. Concurrent callers
|
|
161
|
+
// share the fetchSettings promise; the first to resume here flips the flag (a
|
|
162
|
+
// synchronous step before the next await), so the rest skip the redundant PATCH.
|
|
163
|
+
if (!this.migrationDone) {
|
|
164
|
+
this.migrationDone = true;
|
|
165
|
+
const storedKeys = Object.keys(storedSettings);
|
|
166
|
+
const defaultKeys = Object.keys(defaultSettings);
|
|
167
|
+
if (storedKeys.length !== defaultKeys.length) {
|
|
168
|
+
const validStoredSettings = Object.fromEntries(Object.entries(storedSettings).filter(([key]) => defaultKeys.includes(key)));
|
|
169
|
+
const mergedSettings = { ...defaultSettings, ...validStoredSettings };
|
|
170
|
+
await this.setSettings(mergedSettings);
|
|
171
|
+
return mergedSettings;
|
|
172
|
+
}
|
|
136
173
|
}
|
|
137
174
|
return storedSettings;
|
|
138
175
|
}
|