@nsnanocat/preference-panes 1.1.1 → 1.1.3

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.
@@ -9,6 +9,6 @@
9
9
  </head>
10
10
  <body>
11
11
  <main id="preferences"></main>
12
- <script type="module" src="/settings/assets/index.mjs?v=1.1.1"></script>
12
+ <script type="module" src="/settings/assets/index.mjs?v=1.1.3"></script>
13
13
  </body>
14
14
  </html>
@@ -1,94 +1,3 @@
1
- /**
2
- * 创建元素,所有展示文本通过 textContent 写入。
3
- * Create elements and assign display text through textContent only.
4
- * @template {keyof HTMLElementTagNameMap} T
5
- * @param {T} tag 元素标签 / Element tag.
6
- * @param {string} className 样式类名 / CSS class.
7
- * @param {string} [text] 纯文本 / Plain text.
8
- * @returns {HTMLElementTagNameMap[T]} 创建的元素 / Created element.
9
- */
10
- function element(tag, className, text) {
11
- const node = document.createElement(tag);
12
- node.className = className;
13
- if (text !== undefined) node.textContent = text;
14
- return node;
15
- }
16
-
17
- /**
18
- * 创建通用设置行;外部 CSS 可通过 pp 类名覆盖视觉样式。
19
- * Create a generic settings row whose appearance can be overridden through pp classes.
20
- * @template {"div" | "label"} T
21
- * @param {T} tag 行元素 / Row element.
22
- * @returns {HTMLElementTagNameMap[T]} 设置行 / Settings row.
23
- */
24
- function settingRow(tag) {
25
- return element(tag, "pp-row");
26
- }
27
-
28
- /**
29
- * 为标准 HTML 输入控件添加通用面板类名。
30
- * Add the generic panel class to a standard HTML input control.
31
- * @param {HTMLElement} control 已创建的原生控件 / Existing native control.
32
- * @returns {HTMLElement} 输入控件 / Input control.
33
- */
34
- function fieldControl(control) {
35
- control.classList.add("pp-editor");
36
- return control;
37
- }
38
-
39
- /**
40
- * 元数据地址只允许 HTTP(S) 和相对地址。
41
- * Allow only HTTP(S) and relative metadata addresses.
42
- * @param {string} value 元数据地址 / Metadata address.
43
- * @returns {string} 完整地址 / Absolute address.
44
- */
45
- function resourceURL(value) {
46
- const url = new URL(value, document.baseURI);
47
- if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Metadata URLs must use HTTP(S)");
48
- return url.href;
49
- }
50
-
51
- /**
52
- * 创建覆盖可用内容区的通用读取状态,失败时可附加重试动作。
53
- * Create a shared status view that fills the available content area and may include retry.
54
- * @param {string} message 状态文本 / Status message.
55
- * @param {(() => unknown) | undefined} [retry] 重试动作 / Retry action.
56
- * @returns {HTMLElement} 居中状态视图 / Centered status view.
57
- */
58
- function statusView(message, retry) {
59
- const view = element("section", "pp-status");
60
- view.setAttribute("role", "status");
61
- view.setAttribute("aria-live", "polite");
62
- const spinner = element("span", "pp-status-spinner");
63
- spinner.setAttribute("aria-hidden", "true");
64
- view.append(spinner, element("p", "pp-status-message", message));
65
- if (retry) {
66
- const button = element("button", "pp-status-action", "重新读取");
67
- button.type = "button";
68
- button.onclick = retry;
69
- view.append(button);
70
- }
71
- return view;
72
- }
73
-
74
- /**
75
- * 请求宿主确认;独立网页使用浏览器对话框。
76
- * Request confirmation from the host, using the browser dialog for standalone pages.
77
- * @param {Window} host 模块窗口 / Module window.
78
- * @param {string} message 确认内容 / Confirmation message.
79
- * @returns {Promise<boolean>} 用户是否确认 / Whether the user confirmed.
80
- */
81
- function requestConfirmation(host, message) {
82
- return new Promise((resolve, reject) => {
83
- const frame = host.frameElement;
84
- if (frame) {
85
- const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:confirm", { cancelable: true, detail: { message, resolve, reject } });
86
- if (!frame.dispatchEvent(event)) return;
87
- }
88
- resolve(host.confirm(message));
89
- });
90
- }
91
-
92
1
  /**
93
2
  * 校验原始路径片段,不进行 URL 编码转换。
94
3
  * Validate raw path segments without URL encoding conversion.
@@ -126,6 +35,7 @@ function normalizeBoxJs(config, module) {
126
35
  if (!storageKey || storageKey.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
127
36
  validatePathParts(parts);
128
37
  const name = parts[0];
38
+ if (["get", "set", "delete"].includes(name)) throw new TypeError(`Reserved API module name: ${name}`);
129
39
  let target = modules.get(name);
130
40
  if (!target) {
131
41
  target = { module: name, storageKey, entries: [], owners: new Set() };
@@ -136,8 +46,8 @@ function normalizeBoxJs(config, module) {
136
46
  target.owners.add(app);
137
47
  }
138
48
  }
139
- if (modules.size !== 1) throw new TypeError("Import BoxJS JSON for exactly one module");
140
- const target = modules.values().next().value ;
49
+ if (module === undefined && modules.size !== 1) throw new TypeError("Import BoxJS JSON for exactly one module");
50
+ const target = module === undefined ? modules.values().next().value : modules.get(module);
141
51
  if (!target) throw new TypeError(`No BoxJS settings for module: ${module}`);
142
52
  const metadata = normalizeMetadata(target.owners.size === 1 ? presentation([...target.owners][0]) : {});
143
53
  const fields = [];
@@ -274,6 +184,97 @@ function validValue(field, value) {
274
184
  return !field.options || (field.type === "array" ? value : [value]).every(item => field.options.some(option => option.key === item));
275
185
  }
276
186
 
187
+ /**
188
+ * 创建元素,所有展示文本通过 textContent 写入。
189
+ * Create elements and assign display text through textContent only.
190
+ * @template {keyof HTMLElementTagNameMap} T
191
+ * @param {T} tag 元素标签 / Element tag.
192
+ * @param {string} className 样式类名 / CSS class.
193
+ * @param {string} [text] 纯文本 / Plain text.
194
+ * @returns {HTMLElementTagNameMap[T]} 创建的元素 / Created element.
195
+ */
196
+ function element(tag, className, text) {
197
+ const node = document.createElement(tag);
198
+ node.className = className;
199
+ if (text !== undefined) node.textContent = text;
200
+ return node;
201
+ }
202
+
203
+ /**
204
+ * 创建通用设置行;外部 CSS 可通过 pp 类名覆盖视觉样式。
205
+ * Create a generic settings row whose appearance can be overridden through pp classes.
206
+ * @template {"div" | "label"} T
207
+ * @param {T} tag 行元素 / Row element.
208
+ * @returns {HTMLElementTagNameMap[T]} 设置行 / Settings row.
209
+ */
210
+ function settingRow(tag) {
211
+ return element(tag, "pp-row");
212
+ }
213
+
214
+ /**
215
+ * 为标准 HTML 输入控件添加通用面板类名。
216
+ * Add the generic panel class to a standard HTML input control.
217
+ * @param {HTMLElement} control 已创建的原生控件 / Existing native control.
218
+ * @returns {HTMLElement} 输入控件 / Input control.
219
+ */
220
+ function fieldControl(control) {
221
+ control.classList.add("pp-editor");
222
+ return control;
223
+ }
224
+
225
+ /**
226
+ * 元数据地址只允许 HTTP(S) 和相对地址。
227
+ * Allow only HTTP(S) and relative metadata addresses.
228
+ * @param {string} value 元数据地址 / Metadata address.
229
+ * @returns {string} 完整地址 / Absolute address.
230
+ */
231
+ function resourceURL(value) {
232
+ const url = new URL(value, document.baseURI);
233
+ if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Metadata URLs must use HTTP(S)");
234
+ return url.href;
235
+ }
236
+
237
+ /**
238
+ * 创建覆盖可用内容区的通用读取状态,失败时可附加重试动作。
239
+ * Create a shared status view that fills the available content area and may include retry.
240
+ * @param {string} message 状态文本 / Status message.
241
+ * @param {(() => unknown) | undefined} [retry] 重试动作 / Retry action.
242
+ * @returns {HTMLElement} 居中状态视图 / Centered status view.
243
+ */
244
+ function statusView(message, retry) {
245
+ const view = element("section", "pp-status");
246
+ view.setAttribute("role", "status");
247
+ view.setAttribute("aria-live", "polite");
248
+ const spinner = element("span", "pp-status-spinner");
249
+ spinner.setAttribute("aria-hidden", "true");
250
+ view.append(spinner, element("p", "pp-status-message", message));
251
+ if (retry) {
252
+ const button = element("button", "pp-status-action", "重新读取");
253
+ button.type = "button";
254
+ button.onclick = retry;
255
+ view.append(button);
256
+ }
257
+ return view;
258
+ }
259
+
260
+ /**
261
+ * 请求宿主确认;独立网页使用浏览器对话框。
262
+ * Request confirmation from the host, using the browser dialog for standalone pages.
263
+ * @param {Window} host 模块窗口 / Module window.
264
+ * @param {string} message 确认内容 / Confirmation message.
265
+ * @returns {Promise<boolean>} 用户是否确认 / Whether the user confirmed.
266
+ */
267
+ function requestConfirmation(host, message) {
268
+ return new Promise((resolve, reject) => {
269
+ const frame = host.frameElement;
270
+ if (frame) {
271
+ const event = new frame.ownerDocument.defaultView.CustomEvent("preferencepanes:confirm", { cancelable: true, detail: { message, resolve, reject } });
272
+ if (!frame.dispatchEvent(event)) return;
273
+ }
274
+ resolve(host.confirm(message));
275
+ });
276
+ }
277
+
277
278
  /**
278
279
  * 共用三点按钮和底部操作菜单;弹层挂载到文档根部,不受标题栏显示状态影响。
279
280
  * Shared overflow trigger and bottom action sheet; the layer is mounted at document level and remains independent of header visibility.
@@ -489,7 +490,7 @@ class PreferencesClient {
489
490
  * @returns {Promise<unknown>} Settings 内容或 undefined / Settings content or undefined.
490
491
  */
491
492
  async readSettings() {
492
- const response = await this.#send("get", { scope: "settings" });
493
+ const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#definition.settingsPath.join(".")}`);
493
494
  return response.status === 404 ? undefined : response.json();
494
495
  }
495
496
 
@@ -499,7 +500,7 @@ class PreferencesClient {
499
500
  * @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
500
501
  */
501
502
  async readCaches() {
502
- const response = await this.#send("get", { scope: "caches" });
503
+ const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#module}.Caches`);
503
504
  return response.status === 404 ? undefined : response.json();
504
505
  }
505
506
 
@@ -509,7 +510,7 @@ class PreferencesClient {
509
510
  * @returns {Promise<void>} 操作完成 / Operation completion.
510
511
  */
511
512
  clearCaches() {
512
- return this.#change("delete", { scope: "caches" }, "clearCaches");
513
+ return this.#change("delete", `${this.#module}.Caches`, undefined, "clearCaches");
513
514
  }
514
515
 
515
516
  /**
@@ -518,7 +519,7 @@ class PreferencesClient {
518
519
  * @returns {Promise<void>} 操作完成 / Operation completion.
519
520
  */
520
521
  reset() {
521
- return this.#change("delete", { scope: "module" }, "reset");
522
+ return this.#change("delete", this.#module, undefined, "reset");
522
523
  }
523
524
 
524
525
  /**
@@ -538,7 +539,7 @@ class PreferencesClient {
538
539
  * @returns {Promise<void>} 操作完成 / Operation completion.
539
540
  */
540
541
  set(key, value) {
541
- return this.#change("set", { key, value }, "write", key);
542
+ return this.#change("set", key, value, "write");
542
543
  }
543
544
 
544
545
  /**
@@ -548,30 +549,31 @@ class PreferencesClient {
548
549
  * @returns {Promise<void>} 操作完成 / Operation completion.
549
550
  */
550
551
  remove(key) {
551
- return this.#change("delete", { key }, "delete", key);
552
+ return this.#change("delete", key, undefined, "delete");
552
553
  }
553
554
 
554
555
  /**
555
- * 向模块 API 发送 JSON 动作。
556
- * Send a JSON action to the module API.
557
- * @param {"get" | "set" | "delete"} action 模块动作 / Module action.
558
- * @param {unknown} payload JSON 请求体 / JSON request body.
556
+ * 向固定存储 API 发送完整路径的 form 动作。
557
+ * Send a complete-path form action to the fixed storage API.
558
+ * @param {"get" | "set" | "delete"} action 存储动作 / Storage action.
559
+ * @param {string} path 完整 @root.path / Complete @root.path.
560
+ * @param {unknown} [value] set 写入值 / Value written by set.
559
561
  * @returns {Promise<Response>} 原始响应 / Raw response.
560
562
  */
561
- async #send(action, payload) {
563
+ async #send(action, path, value) {
562
564
  const controller = new AbortController();
563
565
  const abort = () => controller.abort();
564
566
  if (this.#session.signal.aborted) abort();
565
567
  this.#session.signal.addEventListener("abort", abort, { once: true });
566
568
  const timer = setTimeout(abort, this.#timeout);
567
569
  try {
568
- const response = await this.#request(`/api/${encodeURIComponent(this.#module)}/${action}`, {
570
+ const response = await this.#request(`/api/${action}`, {
569
571
  method: "POST",
570
572
  credentials: "omit",
571
573
  cache: "no-store",
572
574
  signal: controller.signal,
573
- headers: { "Content-Type": "application/json" },
574
- body: JSON.stringify(payload),
575
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
576
+ body: new URLSearchParams([[path, action === "set" ? JSON.stringify(value) : ""]]).toString(),
575
577
  });
576
578
  if (response.status !== 200 && !(action === "get" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
577
579
  return response;
@@ -585,28 +587,28 @@ class PreferencesClient {
585
587
  * 执行写入动作;成功后只更新当前页面值。
586
588
  * Execute a mutation and update only the current page values after success.
587
589
  * @param {"set" | "delete"} action API 动作 / API action.
588
- * @param {Record<string, unknown>} payload JSON 请求体 / JSON request body.
590
+ * @param {string} key 不含存储根的路径 / Path without the storage root.
591
+ * @param {unknown} value set 写入值 / Value written by set.
589
592
  * @param {"write" | "delete" | "clearCaches" | "reset"} operation 通知操作 / Notification operation.
590
- * @param {string} [key] 字段路径 / Field path.
591
593
  * @returns {Promise<void>} 操作完成 / Operation completion.
592
594
  */
593
- async #change(action, payload, operation, key) {
595
+ async #change(action, key, value, operation) {
594
596
  if (this.#saving) throw new Error("A settings write is already in progress");
595
597
  this.#saving = true;
596
598
  try {
597
- if (operation === "write") {
598
- const field = this.#definition.fields.find(candidate => candidate.key === key);
599
- if (!field || !validValue(field, payload.value)) throw new TypeError("Invalid setting value");
599
+ let field;
600
+ if (operation === "write" || operation === "delete") {
601
+ field = this.#definition.fields.find(candidate => candidate.key === key);
602
+ if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
600
603
  }
601
- await this.#send(action, payload);
604
+ await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
602
605
  switch (operation) {
603
606
  case "write":
604
- this.#values[key] = structuredClone(payload.value);
607
+ this.#values[key] = structuredClone(value);
605
608
  break;
606
609
  case "delete": {
607
- const field = this.#definition.fields.find(candidate => candidate.key === key);
608
610
  delete this.#values[key];
609
- if (field && Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
611
+ if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
610
612
  break;
611
613
  }
612
614
  case "clearCaches":
@@ -1440,8 +1442,8 @@ class ModulePage {
1440
1442
  }
1441
1443
 
1442
1444
  /**
1443
- * 从规范模块路径读取 BoxJS JSON 并挂载通用前端。
1444
- * Read BoxJS JSON from the conventional module path and mount the generic frontend.
1445
+ * 通过模块 API 读取 BoxJS JSON 并挂载通用前端。
1446
+ * Read BoxJS JSON through the module API and mount the generic frontend.
1445
1447
  * @returns {Promise<void>} 启动完成 / Startup completion.
1446
1448
  */
1447
1449
  async start() {
@@ -1453,9 +1455,11 @@ class ModulePage {
1453
1455
  const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
1454
1456
  const module = embedded ?? match?.[1];
1455
1457
  if (!module) throw new TypeError("Open a concrete module URL");
1456
- const response = await fetch(`/configs/${encodeURIComponent(module)}`, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
1458
+ const response = await fetch(`/api/${encodeURIComponent(module)}`, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
1457
1459
  if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
1458
- this.#view = mount(await response.json());
1460
+ const boxjs = await response.json();
1461
+ normalizeBoxJs(boxjs, module);
1462
+ this.#view = mount(boxjs);
1459
1463
  } catch (error) {
1460
1464
  this.#root.replaceChildren(statusView(`加载失败:${error.message}`, () => this.start()));
1461
1465
  }
@@ -331,7 +331,7 @@ class ModuleStatus extends EventTarget {
331
331
  const response = await probeModule(url, { ...options, signal: controller.signal });
332
332
  if (controller !== this.#controller) return response;
333
333
  const version = response.status === 200 ? response.headers.get("X-PreferencePanes-Version")?.trim() || null : null;
334
- this.#render(response.status === 200 ? "installed" : "missing", version);
334
+ this.#render(version ? "installed" : "missing", version);
335
335
  return response;
336
336
  } catch (error) {
337
337
  if (controller !== this.#controller) return;
@@ -35,6 +35,7 @@ function normalizeBoxJs(config, module) {
35
35
  if (!storageKey || storageKey.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
36
36
  validatePathParts(parts);
37
37
  const name = parts[0];
38
+ if (["get", "set", "delete"].includes(name)) throw new TypeError(`Reserved API module name: ${name}`);
38
39
  let target = modules.get(name);
39
40
  if (!target) {
40
41
  target = { module: name, storageKey, entries: [], owners: new Set() };
@@ -489,7 +490,7 @@ class PreferencesClient {
489
490
  * @returns {Promise<unknown>} Settings 内容或 undefined / Settings content or undefined.
490
491
  */
491
492
  async readSettings() {
492
- const response = await this.#send("get", { scope: "settings" });
493
+ const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#definition.settingsPath.join(".")}`);
493
494
  return response.status === 404 ? undefined : response.json();
494
495
  }
495
496
 
@@ -499,7 +500,7 @@ class PreferencesClient {
499
500
  * @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
500
501
  */
501
502
  async readCaches() {
502
- const response = await this.#send("get", { scope: "caches" });
503
+ const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#module}.Caches`);
503
504
  return response.status === 404 ? undefined : response.json();
504
505
  }
505
506
 
@@ -509,7 +510,7 @@ class PreferencesClient {
509
510
  * @returns {Promise<void>} 操作完成 / Operation completion.
510
511
  */
511
512
  clearCaches() {
512
- return this.#change("delete", { scope: "caches" }, "clearCaches");
513
+ return this.#change("delete", `${this.#module}.Caches`, undefined, "clearCaches");
513
514
  }
514
515
 
515
516
  /**
@@ -518,7 +519,7 @@ class PreferencesClient {
518
519
  * @returns {Promise<void>} 操作完成 / Operation completion.
519
520
  */
520
521
  reset() {
521
- return this.#change("delete", { scope: "module" }, "reset");
522
+ return this.#change("delete", this.#module, undefined, "reset");
522
523
  }
523
524
 
524
525
  /**
@@ -538,7 +539,7 @@ class PreferencesClient {
538
539
  * @returns {Promise<void>} 操作完成 / Operation completion.
539
540
  */
540
541
  set(key, value) {
541
- return this.#change("set", { key, value }, "write", key);
542
+ return this.#change("set", key, value, "write");
542
543
  }
543
544
 
544
545
  /**
@@ -548,30 +549,31 @@ class PreferencesClient {
548
549
  * @returns {Promise<void>} 操作完成 / Operation completion.
549
550
  */
550
551
  remove(key) {
551
- return this.#change("delete", { key }, "delete", key);
552
+ return this.#change("delete", key, undefined, "delete");
552
553
  }
553
554
 
554
555
  /**
555
- * 向模块 API 发送 JSON 动作。
556
- * Send a JSON action to the module API.
557
- * @param {"get" | "set" | "delete"} action 模块动作 / Module action.
558
- * @param {unknown} payload JSON 请求体 / JSON request body.
556
+ * 向固定存储 API 发送完整路径的 form 动作。
557
+ * Send a complete-path form action to the fixed storage API.
558
+ * @param {"get" | "set" | "delete"} action 存储动作 / Storage action.
559
+ * @param {string} path 完整 @root.path / Complete @root.path.
560
+ * @param {unknown} [value] set 写入值 / Value written by set.
559
561
  * @returns {Promise<Response>} 原始响应 / Raw response.
560
562
  */
561
- async #send(action, payload) {
563
+ async #send(action, path, value) {
562
564
  const controller = new AbortController();
563
565
  const abort = () => controller.abort();
564
566
  if (this.#session.signal.aborted) abort();
565
567
  this.#session.signal.addEventListener("abort", abort, { once: true });
566
568
  const timer = setTimeout(abort, this.#timeout);
567
569
  try {
568
- const response = await this.#request(`/api/${encodeURIComponent(this.#module)}/${action}`, {
570
+ const response = await this.#request(`/api/${action}`, {
569
571
  method: "POST",
570
572
  credentials: "omit",
571
573
  cache: "no-store",
572
574
  signal: controller.signal,
573
- headers: { "Content-Type": "application/json" },
574
- body: JSON.stringify(payload),
575
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
576
+ body: new URLSearchParams([[path, action === "set" ? JSON.stringify(value) : ""]]).toString(),
575
577
  });
576
578
  if (response.status !== 200 && !(action === "get" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
577
579
  return response;
@@ -585,28 +587,28 @@ class PreferencesClient {
585
587
  * 执行写入动作;成功后只更新当前页面值。
586
588
  * Execute a mutation and update only the current page values after success.
587
589
  * @param {"set" | "delete"} action API 动作 / API action.
588
- * @param {Record<string, unknown>} payload JSON 请求体 / JSON request body.
590
+ * @param {string} key 不含存储根的路径 / Path without the storage root.
591
+ * @param {unknown} value set 写入值 / Value written by set.
589
592
  * @param {"write" | "delete" | "clearCaches" | "reset"} operation 通知操作 / Notification operation.
590
- * @param {string} [key] 字段路径 / Field path.
591
593
  * @returns {Promise<void>} 操作完成 / Operation completion.
592
594
  */
593
- async #change(action, payload, operation, key) {
595
+ async #change(action, key, value, operation) {
594
596
  if (this.#saving) throw new Error("A settings write is already in progress");
595
597
  this.#saving = true;
596
598
  try {
597
- if (operation === "write") {
598
- const field = this.#definition.fields.find(candidate => candidate.key === key);
599
- if (!field || !validValue(field, payload.value)) throw new TypeError("Invalid setting value");
599
+ let field;
600
+ if (operation === "write" || operation === "delete") {
601
+ field = this.#definition.fields.find(candidate => candidate.key === key);
602
+ if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
600
603
  }
601
- await this.#send(action, payload);
604
+ await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
602
605
  switch (operation) {
603
606
  case "write":
604
- this.#values[key] = structuredClone(payload.value);
607
+ this.#values[key] = structuredClone(value);
605
608
  break;
606
609
  case "delete": {
607
- const field = this.#definition.fields.find(candidate => candidate.key === key);
608
610
  delete this.#values[key];
609
- if (field && Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
611
+ if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
610
612
  break;
611
613
  }
612
614
  case "clearCaches":