@nsnanocat/preference-panes 1.0.0 → 1.1.1
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.
- package/README.md +64 -44
- package/dist/api.js +11 -31
- package/dist/module/index.html +1 -1
- package/dist/module/{app.mjs → index.mjs} +764 -623
- package/dist/module/navigation.mjs +15 -32
- package/dist/preference-panes.mjs +703 -566
- package/dist/web.js +2 -23
- package/package.json +2 -2
- package/src/api.mjs +11 -31
- package/src/browser/ModuleFrame.mjs +13 -13
- package/src/browser/ModuleStatus.mjs +2 -3
- package/src/browser/Navigation.d.mts +2 -4
- package/src/browser/client.d.mts +10 -15
- package/src/browser/client.mjs +154 -48
- package/src/browser/index.d.ts +6 -7
- package/src/browser/index.mjs +59 -79
- package/src/browser/module.html +1 -1
- package/src/browser/mount.mjs +103 -0
- package/src/browser/panel.mjs +464 -444
- package/src/index.d.ts +0 -22
- package/src/index.mjs +3 -3
- package/src/web.mjs +1 -5
- package/src/browser/app.mjs +0 -49
- package/src/build.mjs +0 -20
- package/src/lib/page-inputs.mjs +0 -17
package/src/api.mjs
CHANGED
|
@@ -41,31 +41,21 @@ class API {
|
|
|
41
41
|
const match = /^\/api\/([a-zA-Z0-9_-]+)(?:\/(get|set|delete))?\/?$/.exec(url.pathname);
|
|
42
42
|
if (!match) return;
|
|
43
43
|
const [, module, action] = match;
|
|
44
|
-
const
|
|
44
|
+
const configuration = `${url.origin}/configs/${module}`;
|
|
45
45
|
switch (true) {
|
|
46
46
|
case !action && request.method === "HEAD":
|
|
47
|
-
return this.#probe(request,
|
|
48
|
-
case !action && request.method === "GET":
|
|
49
|
-
return this.#model(request, module, configURL);
|
|
47
|
+
return this.#probe(request, configuration);
|
|
50
48
|
case Boolean(action) && request.method === "POST":
|
|
51
|
-
return this.#action(request, module, action,
|
|
49
|
+
return this.#action(request, module, action, configuration);
|
|
52
50
|
default:
|
|
53
|
-
return this.#response(request, 405, { error: "Use
|
|
51
|
+
return this.#response(request, 405, { error: "Use HEAD for module probes and POST for module actions" });
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
#
|
|
58
|
-
const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
59
|
-
const source = headers["x-preferencepanes-json"] ?? `/configs/${module}`;
|
|
60
|
-
if (/^https?:\/\//i.test(source)) return source;
|
|
61
|
-
if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(source)) throw Object.assign(new TypeError("BoxJS resources must use HTTP(S) URLs"), { status: 400 });
|
|
62
|
-
return `${url.origin}/${source.replace(/^\/+/, "")}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async #probe(request, configURL) {
|
|
55
|
+
async #probe(request, configuration) {
|
|
66
56
|
let result;
|
|
67
57
|
try {
|
|
68
|
-
result = await transport({ url:
|
|
58
|
+
result = await transport({ url: configuration, method: "HEAD", timeout: 5000, headers: { Accept: "application/json" } });
|
|
69
59
|
} catch (error) {
|
|
70
60
|
return this.#response(request, 502, { error: error.message });
|
|
71
61
|
}
|
|
@@ -73,19 +63,9 @@ class API {
|
|
|
73
63
|
return this.#response(request, result.statusCode ?? result.status, undefined, version ? { "X-PreferencePanes-Version": version } : {});
|
|
74
64
|
}
|
|
75
65
|
|
|
76
|
-
async #
|
|
77
|
-
const loaded = await this.#load(module, configURL);
|
|
78
|
-
const values = {};
|
|
79
|
-
for (const entry of loaded.entries) {
|
|
80
|
-
const value = Storage.getItem(entry.id, MISSING);
|
|
81
|
-
if (value !== MISSING) values[entry.id.slice(loaded.storageKey.length + 2)] = value;
|
|
82
|
-
}
|
|
83
|
-
return this.#response(request, 200, { module, boxjs: loaded.boxjs, values, configURL }, loaded.version ? { "X-PreferencePanes-Version": loaded.version } : {});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async #action(request, module, action, configURL) {
|
|
66
|
+
async #action(request, module, action, configuration) {
|
|
87
67
|
const payload = this.#jsonBody(request);
|
|
88
|
-
const target = await this.#load(module,
|
|
68
|
+
const target = await this.#load(module, configuration);
|
|
89
69
|
switch (action) {
|
|
90
70
|
case "get": {
|
|
91
71
|
const value = Storage.getItem(payload?.scope ? this.#scopePath(target, payload.scope) : this.#storagePath(target, payload?.key), MISSING);
|
|
@@ -103,10 +83,10 @@ class API {
|
|
|
103
83
|
}
|
|
104
84
|
}
|
|
105
85
|
|
|
106
|
-
async #load(module,
|
|
86
|
+
async #load(module, configuration) {
|
|
107
87
|
let result;
|
|
108
88
|
try {
|
|
109
|
-
result = await transport({ url:
|
|
89
|
+
result = await transport({ url: configuration, method: "GET", timeout: 5000, headers: { Accept: "application/json" } });
|
|
110
90
|
} catch (error) {
|
|
111
91
|
throw Object.assign(new Error(`Configuration request failed: ${error.message}`), { status: 502 });
|
|
112
92
|
}
|
|
@@ -137,7 +117,7 @@ class API {
|
|
|
137
117
|
}
|
|
138
118
|
}
|
|
139
119
|
if (!entries.length) throw new TypeError(`No BoxJS settings for module: ${module}`);
|
|
140
|
-
return {
|
|
120
|
+
return { entries, module, storageKey, version: this.#header(result.headers, "x-preferencepanes-version") };
|
|
141
121
|
} catch (error) {
|
|
142
122
|
throw Object.assign(new Error(`Invalid BoxJS: ${error.message}`), { status: 422 });
|
|
143
123
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { pageInputs } from "../lib/page-inputs.mjs";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
* 模块文档容器:原始 HTML
|
|
5
|
-
* Module document container: preserve HTML verbatim and
|
|
2
|
+
* 模块文档容器:原始 HTML 不改写,只向 iframe 标记模块身份。
|
|
3
|
+
* Module document container: preserve HTML verbatim and mark only the module identity on the iframe.
|
|
6
4
|
*/
|
|
7
5
|
export class ModuleFrame extends EventTarget {
|
|
8
6
|
#url;
|
|
@@ -24,23 +22,25 @@ export class ModuleFrame extends EventTarget {
|
|
|
24
22
|
};
|
|
25
23
|
|
|
26
24
|
/**
|
|
27
|
-
* 建立 iframe
|
|
28
|
-
* Create the iframe
|
|
25
|
+
* 建立 iframe;调用方挂载 element 后调用 load。
|
|
26
|
+
* Create the iframe; callers mount element and then call load.
|
|
29
27
|
* @param {string | URL} url 模块请求地址 / Module request URL.
|
|
30
|
-
* @param {
|
|
28
|
+
* @param {{signal?: AbortSignal}} [options] 外部取消信号 / External cancellation signal.
|
|
31
29
|
*/
|
|
32
30
|
constructor(url, options = {}) {
|
|
33
31
|
super();
|
|
34
32
|
this.#url = new URL(url, document.baseURI);
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#url.pathname);
|
|
34
|
+
if (!match) throw new TypeError("Open a concrete module URL");
|
|
35
|
+
this.#options = options;
|
|
37
36
|
this.element = document.createElement("iframe");
|
|
38
|
-
this.element.title = `${
|
|
39
|
-
this.element.dataset.preferencePanes =
|
|
37
|
+
this.element.title = `${match[1]} 设置`;
|
|
38
|
+
this.element.dataset.preferencePanes = "true";
|
|
39
|
+
this.element.dataset.preferencePanesModule = match[1];
|
|
40
40
|
this.element.addEventListener("preferencepanes:change", this.#change);
|
|
41
41
|
this.element.addEventListener("preferencepanes:confirm", this.#confirmation);
|
|
42
42
|
this.element.addEventListener("preferencepanes:notice", this.#notice);
|
|
43
|
-
this.#state = { title:
|
|
43
|
+
this.#state = { title: match[1], module: match[1], busy: false, canGoBack: true, actions: [] };
|
|
44
44
|
options.signal?.addEventListener("abort", this.#abort, { once: true });
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -61,7 +61,7 @@ export class ModuleFrame extends EventTarget {
|
|
|
61
61
|
if (this.#options.signal?.aborted) this.destroy();
|
|
62
62
|
const timer = setTimeout(() => this.#controller.abort(), 10000);
|
|
63
63
|
try {
|
|
64
|
-
const response = await fetch(this.#url, { cache: "no-store", credentials: "omit",
|
|
64
|
+
const response = await fetch(this.#url, { cache: "no-store", credentials: "omit", signal: this.#controller.signal });
|
|
65
65
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
66
66
|
const html = await response.text();
|
|
67
67
|
this.#controller.signal.throwIfAborted();
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* @typedef {object} ModuleProbeOptions
|
|
5
5
|
* @property {typeof globalThis.fetch} [fetch] 可注入的 fetch / Injectable fetch.
|
|
6
6
|
* @property {AbortSignal} [signal] 外部取消信号 / External cancellation signal.
|
|
7
|
-
* @property {string} [json] BoxJS JSON 来源,将随探测请求头传递 / BoxJS JSON source sent in the probe header.
|
|
8
7
|
* @property {number} [timeout] 超时毫秒数,默认 3500 / Timeout in milliseconds, defaults to 3500.
|
|
9
8
|
*/
|
|
10
9
|
|
|
@@ -15,14 +14,14 @@
|
|
|
15
14
|
* @param {ModuleProbeOptions} [options] 请求选项 / Request options.
|
|
16
15
|
* @returns {Promise<Response>} 原始 HTTP 响应,可直接读取 status 和响应头 / Native HTTP response; read status and headers directly.
|
|
17
16
|
*/
|
|
18
|
-
export async function probeModule(url, { fetch: request = globalThis.fetch,
|
|
17
|
+
export async function probeModule(url, { fetch: request = globalThis.fetch, signal, timeout = 3500 } = {}) {
|
|
19
18
|
const controller = new AbortController();
|
|
20
19
|
const abort = () => controller.abort();
|
|
21
20
|
if (signal?.aborted) abort();
|
|
22
21
|
signal?.addEventListener("abort", abort, { once: true });
|
|
23
22
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
24
23
|
try {
|
|
25
|
-
return await request(url, { method: "HEAD", cache: "no-store", credentials: "omit", signal: controller.signal
|
|
24
|
+
return await request(url, { method: "HEAD", cache: "no-store", credentials: "omit", signal: controller.signal });
|
|
26
25
|
} finally {
|
|
27
26
|
clearTimeout(timer);
|
|
28
27
|
signal?.removeEventListener("abort", abort);
|
|
@@ -11,9 +11,9 @@ export class ModuleFrame extends EventTarget {
|
|
|
11
11
|
* 创建容器。
|
|
12
12
|
* Create a container.
|
|
13
13
|
* @param url 模块地址 / Module URL.
|
|
14
|
-
* @param options
|
|
14
|
+
* @param options 外部取消选项 / External cancellation options.
|
|
15
15
|
*/
|
|
16
|
-
constructor(url: string | URL, options?:
|
|
16
|
+
constructor(url: string | URL, options?: { signal?: AbortSignal });
|
|
17
17
|
/**
|
|
18
18
|
* 宿主挂载节点。
|
|
19
19
|
* Host-mounted element.
|
|
@@ -169,8 +169,6 @@ export interface ModuleProbeOptions {
|
|
|
169
169
|
fetch?: typeof globalThis.fetch;
|
|
170
170
|
/** 外部取消信号 / External cancellation signal. */
|
|
171
171
|
signal?: AbortSignal;
|
|
172
|
-
/** 传给模块 API 的 BoxJS JSON 来源 / BoxJS JSON source sent to the module API. */
|
|
173
|
-
json?: string;
|
|
174
172
|
/** 超时毫秒数,默认 3500 / Timeout in milliseconds, defaults to 3500. */
|
|
175
173
|
timeout?: number;
|
|
176
174
|
}
|
package/src/browser/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ModuleDefinition,
|
|
1
|
+
import type { ModuleDefinition, SettingsScalar } from "../index.js";
|
|
2
2
|
/**
|
|
3
3
|
* 单键写入或删除的通知事件。
|
|
4
4
|
* Notification for a single-key write or delete.
|
|
@@ -16,12 +16,10 @@ export interface Notification {
|
|
|
16
16
|
message?: string;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
* Browser page client options; the
|
|
19
|
+
* 浏览器页面客户端选项;字段定义来自 BoxJS。
|
|
20
|
+
* Browser page client options; the field definition comes from BoxJS.
|
|
21
21
|
*/
|
|
22
22
|
export interface PreferencesClientOptions {
|
|
23
|
-
/** API 返回的模块模型 / Module model returned by the API. */
|
|
24
|
-
model: ModuleModel;
|
|
25
23
|
/** 用于渲染的归一化字段定义 / Normalized field definition for rendering. */
|
|
26
24
|
definition: ModuleDefinition;
|
|
27
25
|
/** 默认使用浏览器 fetch / Defaults to browser fetch. */
|
|
@@ -42,10 +40,14 @@ export interface ModuleSnapshot {
|
|
|
42
40
|
values: Record<string, SettingsScalar | SettingsScalar[] | null>;
|
|
43
41
|
}
|
|
44
42
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* 管理单模块页面的 API 请求、值快照和会话终止。
|
|
44
|
+
* Manage API requests, value snapshots, and session termination for one module page.
|
|
47
45
|
*/
|
|
48
|
-
export
|
|
46
|
+
export class PreferencesClient {
|
|
47
|
+
/** 创建页面客户端 / Create the page client. */
|
|
48
|
+
constructor(options: PreferencesClientOptions);
|
|
49
|
+
/** 读取设置并建立页面快照 / Read settings and establish the page snapshot. */
|
|
50
|
+
open(): Promise<ModuleSnapshot>;
|
|
49
51
|
/** 获取页面快照,不发请求 / Get a page snapshot without a request. */
|
|
50
52
|
snapshot(): ModuleSnapshot;
|
|
51
53
|
/** 读取 Settings 子树 / Read the Settings subtree. */
|
|
@@ -63,10 +65,3 @@ export interface PreferencesClient {
|
|
|
63
65
|
/** 删除单个字段覆盖值 / Delete one field override. */
|
|
64
66
|
remove(key: string): Promise<void>;
|
|
65
67
|
}
|
|
66
|
-
/**
|
|
67
|
-
* 创建只调用模块 API 的页面客户端。
|
|
68
|
-
* Create a page client that only calls the module API.
|
|
69
|
-
* @param options API 模型与运行环境 / API model and runtime.
|
|
70
|
-
* @returns 页面客户端 / Page client.
|
|
71
|
-
*/
|
|
72
|
-
export function createPreferencesClient(options: PreferencesClientOptions): PreferencesClient;
|
package/src/browser/client.mjs
CHANGED
|
@@ -1,14 +1,133 @@
|
|
|
1
|
+
import { normalizeStoredValue, validValue } from "./boxjs.mjs";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @param {import("./client.mjs").PreferencesClientOptions} options API 模型、请求与通知 / API model, requests and notifications.
|
|
5
|
-
* @returns {import("./client.mjs").PreferencesClient} 页面客户端 / Page client.
|
|
4
|
+
* 管理单模块页面的 API 请求、值快照和会话终止。
|
|
5
|
+
* Manage API requests, value snapshots, and session termination for one module page.
|
|
6
6
|
*/
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
export class PreferencesClient {
|
|
8
|
+
#module;
|
|
9
|
+
#definition;
|
|
10
|
+
#request;
|
|
11
|
+
#notify;
|
|
12
|
+
#timeout;
|
|
13
|
+
#session = new AbortController();
|
|
14
|
+
#values = {};
|
|
15
|
+
#saving = false;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 创建从 BoxJS 定义读取和持久化设置的页面客户端。
|
|
19
|
+
* Create a page client that reads and persists settings from a BoxJS definition.
|
|
20
|
+
* @param {import("./client.mjs").PreferencesClientOptions} options 字段定义、请求与通知 / Field definition, requests, and notifications.
|
|
21
|
+
*/
|
|
22
|
+
constructor({ definition, fetch: request = globalThis.fetch.bind(globalThis), notify = () => {}, timeout = 10000 }) {
|
|
23
|
+
this.#module = definition.module;
|
|
24
|
+
this.#definition = definition;
|
|
25
|
+
this.#request = request;
|
|
26
|
+
this.#notify = notify;
|
|
27
|
+
this.#timeout = timeout;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 读取一次 Settings 子树并建立页面值快照。
|
|
32
|
+
* Read the Settings subtree once and establish the page value snapshot.
|
|
33
|
+
* @returns {Promise<import("./client.mjs").ModuleSnapshot>} 页面快照 / Page snapshot.
|
|
34
|
+
*/
|
|
35
|
+
async open() {
|
|
36
|
+
let subtree = await this.readSettings();
|
|
37
|
+
if (subtree === undefined) subtree = {};
|
|
38
|
+
if (typeof subtree === "string") subtree = JSON.parse(subtree);
|
|
39
|
+
if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
|
|
40
|
+
const values = {};
|
|
41
|
+
for (const field of this.#definition.fields) {
|
|
42
|
+
const stored = field.key
|
|
43
|
+
.split(".")
|
|
44
|
+
.slice(this.#definition.settingsPath.length)
|
|
45
|
+
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
46
|
+
const value = normalizeStoredValue(field, stored === undefined ? field.defaultValue : stored);
|
|
47
|
+
if (value === undefined) continue;
|
|
48
|
+
if (!validValue(field, value)) throw new TypeError(`Invalid stored value: ${field.key}`);
|
|
49
|
+
values[field.key] = value;
|
|
50
|
+
}
|
|
51
|
+
this.#values = values;
|
|
52
|
+
return this.snapshot();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 获取当前字段定义和值的深拷贝,不发起网络请求。
|
|
57
|
+
* Return a deep copy of the current field definition and values without a network request.
|
|
58
|
+
* @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
|
|
59
|
+
*/
|
|
60
|
+
snapshot() {
|
|
61
|
+
return structuredClone({ definition: this.#definition, values: this.#values });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 读取 Settings 子树。
|
|
66
|
+
* Read the Settings subtree.
|
|
67
|
+
* @returns {Promise<unknown>} Settings 内容或 undefined / Settings content or undefined.
|
|
68
|
+
*/
|
|
69
|
+
async readSettings() {
|
|
70
|
+
const response = await this.#send("get", { scope: "settings" });
|
|
71
|
+
return response.status === 404 ? undefined : response.json();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 读取 Caches 子树。
|
|
76
|
+
* Read the Caches subtree.
|
|
77
|
+
* @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
|
|
78
|
+
*/
|
|
79
|
+
async readCaches() {
|
|
80
|
+
const response = await this.#send("get", { scope: "caches" });
|
|
81
|
+
return response.status === 404 ? undefined : response.json();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 删除当前模块的 Caches 子树。
|
|
86
|
+
* Delete the current module Caches subtree.
|
|
87
|
+
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
88
|
+
*/
|
|
89
|
+
clearCaches() {
|
|
90
|
+
return this.#change("delete", { scope: "caches" }, "clearCaches");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 删除当前模块数据并恢复页面默认值。
|
|
95
|
+
* Delete current module data and restore page defaults.
|
|
96
|
+
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
97
|
+
*/
|
|
98
|
+
reset() {
|
|
99
|
+
return this.#change("delete", { scope: "module" }, "reset");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 终止当前页面仍在进行的请求。
|
|
104
|
+
* Abort requests still owned by the current page.
|
|
105
|
+
* @returns {void} 无返回值 / No return value.
|
|
106
|
+
*/
|
|
107
|
+
leave() {
|
|
108
|
+
this.#session.abort();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 写入单个字段。
|
|
113
|
+
* Write one field.
|
|
114
|
+
* @param {string} key 字段路径 / Field path.
|
|
115
|
+
* @param {unknown} value 已校验值 / Validated value.
|
|
116
|
+
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
117
|
+
*/
|
|
118
|
+
set(key, value) {
|
|
119
|
+
return this.#change("set", { key, value }, "write", key);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 删除单个字段覆盖值。
|
|
124
|
+
* Delete one field override.
|
|
125
|
+
* @param {string} key 字段路径 / Field path.
|
|
126
|
+
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
127
|
+
*/
|
|
128
|
+
remove(key) {
|
|
129
|
+
return this.#change("delete", { key }, "delete", key);
|
|
130
|
+
}
|
|
12
131
|
|
|
13
132
|
/**
|
|
14
133
|
* 向模块 API 发送 JSON 动作。
|
|
@@ -17,26 +136,26 @@ export function createPreferencesClient({ model, definition, fetch: request = gl
|
|
|
17
136
|
* @param {unknown} payload JSON 请求体 / JSON request body.
|
|
18
137
|
* @returns {Promise<Response>} 原始响应 / Raw response.
|
|
19
138
|
*/
|
|
20
|
-
async
|
|
139
|
+
async #send(action, payload) {
|
|
21
140
|
const controller = new AbortController();
|
|
22
141
|
const abort = () => controller.abort();
|
|
23
|
-
if (session.signal.aborted) abort();
|
|
24
|
-
session.signal.addEventListener("abort", abort, { once: true });
|
|
25
|
-
const timer = setTimeout(abort, timeout);
|
|
142
|
+
if (this.#session.signal.aborted) abort();
|
|
143
|
+
this.#session.signal.addEventListener("abort", abort, { once: true });
|
|
144
|
+
const timer = setTimeout(abort, this.#timeout);
|
|
26
145
|
try {
|
|
27
|
-
const response = await request(`/api/${encodeURIComponent(module)}/${action}`, {
|
|
146
|
+
const response = await this.#request(`/api/${encodeURIComponent(this.#module)}/${action}`, {
|
|
28
147
|
method: "POST",
|
|
29
148
|
credentials: "omit",
|
|
30
149
|
cache: "no-store",
|
|
31
150
|
signal: controller.signal,
|
|
32
|
-
headers: { "Content-Type": "application/json"
|
|
151
|
+
headers: { "Content-Type": "application/json" },
|
|
33
152
|
body: JSON.stringify(payload),
|
|
34
153
|
});
|
|
35
154
|
if (response.status !== 200 && !(action === "get" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
|
|
36
155
|
return response;
|
|
37
156
|
} finally {
|
|
38
157
|
clearTimeout(timer);
|
|
39
|
-
session.signal.removeEventListener("abort", abort);
|
|
158
|
+
this.#session.signal.removeEventListener("abort", abort);
|
|
40
159
|
}
|
|
41
160
|
}
|
|
42
161
|
|
|
@@ -44,58 +163,45 @@ export function createPreferencesClient({ model, definition, fetch: request = gl
|
|
|
44
163
|
* 执行写入动作;成功后只更新当前页面值。
|
|
45
164
|
* Execute a mutation and update only the current page values after success.
|
|
46
165
|
* @param {"set" | "delete"} action API 动作 / API action.
|
|
47
|
-
* @param {unknown} payload JSON 请求体 / JSON request body.
|
|
166
|
+
* @param {Record<string, unknown>} payload JSON 请求体 / JSON request body.
|
|
48
167
|
* @param {"write" | "delete" | "clearCaches" | "reset"} operation 通知操作 / Notification operation.
|
|
49
168
|
* @param {string} [key] 字段路径 / Field path.
|
|
50
169
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
51
170
|
*/
|
|
52
|
-
async
|
|
53
|
-
if (saving) throw new Error("A settings write is already in progress");
|
|
54
|
-
saving = true;
|
|
171
|
+
async #change(action, payload, operation, key) {
|
|
172
|
+
if (this.#saving) throw new Error("A settings write is already in progress");
|
|
173
|
+
this.#saving = true;
|
|
55
174
|
try {
|
|
56
|
-
|
|
175
|
+
if (operation === "write") {
|
|
176
|
+
const field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
177
|
+
if (!field || !validValue(field, payload.value)) throw new TypeError("Invalid setting value");
|
|
178
|
+
}
|
|
179
|
+
await this.#send(action, payload);
|
|
57
180
|
switch (operation) {
|
|
58
181
|
case "write":
|
|
59
|
-
values[key] = structuredClone(payload.value);
|
|
182
|
+
this.#values[key] = structuredClone(payload.value);
|
|
60
183
|
break;
|
|
61
184
|
case "delete": {
|
|
62
|
-
const field = definition.fields.find(candidate => candidate.key === key);
|
|
63
|
-
delete values[key];
|
|
64
|
-
if (field && Object.hasOwn(field, "defaultValue")) values[key] = structuredClone(field.defaultValue);
|
|
185
|
+
const field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
186
|
+
delete this.#values[key];
|
|
187
|
+
if (field && Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
|
|
65
188
|
break;
|
|
66
189
|
}
|
|
67
190
|
case "clearCaches":
|
|
68
191
|
break;
|
|
69
192
|
case "reset":
|
|
70
|
-
for (const field of definition.fields) {
|
|
71
|
-
delete values[field.key];
|
|
72
|
-
if (Object.hasOwn(field, "defaultValue")) values[field.key] = structuredClone(field.defaultValue);
|
|
193
|
+
for (const field of this.#definition.fields) {
|
|
194
|
+
delete this.#values[field.key];
|
|
195
|
+
if (Object.hasOwn(field, "defaultValue")) this.#values[field.key] = structuredClone(field.defaultValue);
|
|
73
196
|
}
|
|
74
197
|
break;
|
|
75
198
|
}
|
|
76
|
-
notify({ kind: "success", operation, module, key });
|
|
199
|
+
this.#notify({ kind: "success", operation, module: this.#module, key });
|
|
77
200
|
} catch (error) {
|
|
78
|
-
notify({ kind: "error", operation, module, key, message: error.message });
|
|
201
|
+
this.#notify({ kind: "error", operation, module: this.#module, key, message: error.message });
|
|
79
202
|
throw error;
|
|
80
203
|
} finally {
|
|
81
|
-
saving = false;
|
|
204
|
+
this.#saving = false;
|
|
82
205
|
}
|
|
83
206
|
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
snapshot: () => structuredClone({ definition, values }),
|
|
87
|
-
async readSettings() {
|
|
88
|
-
const response = await send("get", { scope: "settings" });
|
|
89
|
-
return response.status === 404 ? undefined : response.json();
|
|
90
|
-
},
|
|
91
|
-
async readCaches() {
|
|
92
|
-
const response = await send("get", { scope: "caches" });
|
|
93
|
-
return response.status === 404 ? undefined : response.json();
|
|
94
|
-
},
|
|
95
|
-
clearCaches: () => change("delete", { scope: "caches" }, "clearCaches"),
|
|
96
|
-
reset: () => change("delete", { scope: "module" }, "reset"),
|
|
97
|
-
leave: () => session.abort(),
|
|
98
|
-
set: (key, value) => change("set", { key, value }, "write", key),
|
|
99
|
-
remove: key => change("delete", { key }, "delete", key),
|
|
100
|
-
};
|
|
101
207
|
}
|
package/src/browser/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BoxJSInput } from "../index.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 具体模块设置页的生命周期句柄。
|
|
@@ -9,10 +9,9 @@ export interface MountedPreferences {
|
|
|
9
9
|
destroy(): void;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* Mount a settings page from
|
|
14
|
-
* @param
|
|
15
|
-
* @
|
|
16
|
-
* @returns 生命周期句柄 / Lifecycle handle.
|
|
12
|
+
* 使用原始 BoxJS JSON 挂载设置页。
|
|
13
|
+
* Mount a settings page from raw BoxJS JSON.
|
|
14
|
+
* @param boxjs 恰好包含一个模块的 BoxJS JSON / BoxJS JSON describing exactly one module.
|
|
15
|
+
* @returns 模块视图 / Module view.
|
|
17
16
|
*/
|
|
18
|
-
export function mount(
|
|
17
|
+
export function mount(boxjs: BoxJSInput): MountedPreferences;
|