@rimori/client 2.5.46-next.0 → 2.5.47-next.0

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.
@@ -107,6 +107,12 @@ export interface RimoriPluginConfig<T extends object = object> {
107
107
  user_path: string;
108
108
  /** Path to developer documentation for plugin development */
109
109
  developer_path: string;
110
+ /**
111
+ * Optional path to side-panel / browser-extension specific usage notes. Kept separate so it
112
+ * can be shown on its own when the plugin runs in the extension side panel, and appended to
113
+ * the main user docs when read in the full app — avoiding a duplicated description.
114
+ */
115
+ sidebar_path?: string;
110
116
  };
111
117
  /**
112
118
  * Configuration for the plugin's web worker if it uses background processing or exposes actions to other plugins.
@@ -54,6 +54,14 @@ export declare class PluginModule {
54
54
  * @throws {Error} if RLS blocks the operation.
55
55
  */
56
56
  setSettings(settings: any): Promise<void>;
57
+ /**
58
+ * Seeds the plugin's default settings row without ever clobbering an existing one.
59
+ * Used by getSettings when no row was read. Inserts the defaults; if a concurrent
60
+ * writer already created the row (unique-violation) — or it existed all along and
61
+ * the read merely missed it — re-reads and returns the persisted settings instead
62
+ * of the defaults. This keeps the read-triggered seed idempotent and race-safe.
63
+ */
64
+ private seedDefaultSettings;
57
65
  /**
58
66
  * Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
59
67
  * @param defaultSettings The default settings to use if no settings are found.
@@ -146,6 +146,44 @@ export class PluginModule {
146
146
  throw insertError;
147
147
  }
148
148
  }
149
+ /**
150
+ * Seeds the plugin's default settings row without ever clobbering an existing one.
151
+ * Used by getSettings when no row was read. Inserts the defaults; if a concurrent
152
+ * writer already created the row (unique-violation) — or it existed all along and
153
+ * the read merely missed it — re-reads and returns the persisted settings instead
154
+ * of the defaults. This keeps the read-triggered seed idempotent and race-safe.
155
+ */
156
+ async seedDefaultSettings(defaults) {
157
+ const isGuildSetting = !this.rimoriInfo.guild.allowUserPluginSettings;
158
+ const payload = {
159
+ plugin_id: this.pluginId,
160
+ settings: defaults,
161
+ guild_id: this.rimoriInfo.guild.id,
162
+ is_guild_setting: isGuildSetting,
163
+ };
164
+ if (isGuildSetting) {
165
+ payload.user_id = null;
166
+ }
167
+ const { error } = await this.supabase.schema('public').from('plugin_settings').insert(payload);
168
+ if (!error) {
169
+ // Keep the read cache consistent with what we just persisted.
170
+ this.settingsPromise = Promise.resolve(defaults);
171
+ return defaults;
172
+ }
173
+ // Row already exists (another instance won the race, or the earlier read missed
174
+ // it). Adopt the persisted row rather than overwriting it with our defaults.
175
+ if (error.code === '23505' /* unique_violation */) {
176
+ this.settingsPromise = undefined; // force a fresh refetch
177
+ const existing = await this.fetchSettings();
178
+ return existing ?? defaults;
179
+ }
180
+ // Genuine failure — drop the optimistic cache so reads refetch the truth.
181
+ this.settingsPromise = undefined;
182
+ if (error.code === '42501' || error.message?.includes('policy')) {
183
+ throw new Error(`Cannot set ${isGuildSetting ? 'guild' : 'user'} settings: Permission denied.`);
184
+ }
185
+ throw error;
186
+ }
149
187
  /**
150
188
  * Get the settings for the plugin. T can be any type of settings, UserSettings or SystemSettings.
151
189
  * @param defaultSettings The default settings to use if no settings are found.
@@ -154,8 +192,17 @@ export class PluginModule {
154
192
  async getSettings(defaultSettings) {
155
193
  const storedSettings = await this.fetchSettings();
156
194
  if (!storedSettings) {
157
- await this.setSettings(defaultSettings);
158
- return defaultSettings;
195
+ // No row read — seed defaults, but NEVER overwrite on conflict. During first
196
+ // onboarding several independent PluginModule instances read settings at once:
197
+ // the federated main-panel hooks AND the plugin worker, which runs in its own
198
+ // runtime with its own client + single-flight cache and so doesn't share the
199
+ // in-flight dedupe above. They all reach this null branch before any row exists
200
+ // and race to create it. An unconditional write here both 409s on the unique
201
+ // index (ux_plugin_settings_guild_plugin_user) and lets a loser clobber a
202
+ // freshly-persisted row (e.g. is_inited:true) back to defaults. Insert-if-absent
203
+ // and adopt the existing row on conflict — this also guards against a transient
204
+ // read miss (fetchSettings swallows errors → null) overwriting good data.
205
+ return (await this.seedDefaultSettings(defaultSettings));
159
206
  }
160
207
  // Key-count migration runs at most once per plugin lifetime. Concurrent callers
161
208
  // share the fetchSettings promise; the first to resume here flips the flag (a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimori/client",
3
- "version": "2.5.46-next.0",
3
+ "version": "2.5.47-next.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {
@@ -51,6 +51,7 @@
51
51
  "prebuild": "pnpm sync-db-types",
52
52
  "predev": "pnpm sync-db-types",
53
53
  "build": "tsc",
54
+ "check": "tsc --noEmit --pretty",
54
55
  "dev": "tsc -w --preserveWatchOutput",
55
56
  "lint": "pnpm exec eslint . --fix",
56
57
  "format": "prettier --write .",