@nsnanocat/preference-panes 0.9.16 → 1.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nsnanocat/preference-panes",
3
- "version": "0.9.16",
3
+ "version": "1.1.0",
4
4
  "description": "Shared settings API runtime for JavaScript proxy modules",
5
5
  "author": "VirgilClyne <Virgil@nanocat.me>",
6
6
  "homepage": "https://NSNanoCat.github.io/preference-panes",
package/src/api.mjs ADDED
@@ -0,0 +1,191 @@
1
+ import { URL } from "@nsnanocat/url";
2
+ import { fetch as transport } from "@nsnanocat/util";
3
+ import { $app } from "@nsnanocat/util/lib/app.mjs";
4
+ import { done } from "@nsnanocat/util/lib/done.mjs";
5
+ import { Storage } from "@nsnanocat/util/polyfill/Storage";
6
+ import { validatePathParts } from "./lib/settings-path.mjs";
7
+
8
+ const MISSING = Symbol("missing");
9
+
10
+ /**
11
+ * PreferencePanes 后端 API,只处理模块数据和持久化请求。
12
+ * PreferencePanes backend API handling only module data and persistence requests.
13
+ */
14
+ class API {
15
+ /**
16
+ * 处理当前代理请求并将结果交给宿主。
17
+ * Handle the current proxy request and deliver its result to the host.
18
+ * @returns {Promise<void>} 响应已交给代理宿主 / Response delivered to the proxy host.
19
+ */
20
+ async run() {
21
+ const request = globalThis.$request;
22
+ let result;
23
+ try {
24
+ result = await this.handle(request);
25
+ } catch (error) {
26
+ console.error(`PreferencePanes: ${error.message}`);
27
+ result = this.#response(request, error.status ?? 500, { error: error.message });
28
+ }
29
+ if (!result) done({});
30
+ else done($app === "Quantumult X" ? result : { response: result });
31
+ }
32
+
33
+ /**
34
+ * 处理 `/api/{module}` 及其动作,不接管页面或静态资源。
35
+ * Handle `/api/{module}` and its actions without intercepting pages or static assets.
36
+ * @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
37
+ * @returns {Promise<import("./index.js").SettingsResponse | undefined>} API 响应或未接管 / API response or pass-through.
38
+ */
39
+ async handle(request) {
40
+ const url = new URL(request.url);
41
+ const match = /^\/api\/([a-zA-Z0-9_-]+)(?:\/(get|set|delete))?\/?$/.exec(url.pathname);
42
+ if (!match) return;
43
+ const [, module, action] = match;
44
+ const configURL = this.#configURL(request, url, module);
45
+ switch (true) {
46
+ case !action && request.method === "HEAD":
47
+ return this.#probe(request, configURL);
48
+ case !action && request.method === "GET":
49
+ return this.#model(request, module, configURL);
50
+ case Boolean(action) && request.method === "POST":
51
+ return this.#action(request, module, action, configURL);
52
+ default:
53
+ return this.#response(request, 405, { error: "Use GET or HEAD for module reads, POST for module actions" });
54
+ }
55
+ }
56
+
57
+ #configURL(request, url, module) {
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) {
66
+ let result;
67
+ try {
68
+ result = await transport({ url: configURL, method: "HEAD", timeout: 5000, headers: { Accept: "application/json" } });
69
+ } catch (error) {
70
+ return this.#response(request, 502, { error: error.message });
71
+ }
72
+ const version = this.#header(result.headers, "x-preferencepanes-version");
73
+ return this.#response(request, result.statusCode ?? result.status, undefined, version ? { "X-PreferencePanes-Version": version } : {});
74
+ }
75
+
76
+ async #model(request, module, configURL) {
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) {
87
+ const payload = this.#jsonBody(request);
88
+ const target = await this.#load(module, configURL);
89
+ switch (action) {
90
+ case "get": {
91
+ const value = Storage.getItem(payload?.scope ? this.#scopePath(target, payload.scope) : this.#storagePath(target, payload?.key), MISSING);
92
+ return value === MISSING ? this.#response(request, 404, { error: "Stored path does not exist" }) : this.#response(request, 200, value);
93
+ }
94
+ case "set":
95
+ if (!Object.hasOwn(payload ?? {}, "value")) throw Object.assign(new TypeError("A value is required"), { status: 400 });
96
+ if (!Storage.setItem(this.#storagePath(target, payload.key), payload.value)) throw new Error("Storage write failed");
97
+ return this.#response(request, 200, { saved: true });
98
+ case "delete": {
99
+ const path = payload?.scope ? this.#scopePath(target, payload.scope) : this.#storagePath(target, payload?.key);
100
+ if (!Storage.removeItem(path)) throw new Error("Storage write failed");
101
+ return this.#response(request, 200, { deleted: true });
102
+ }
103
+ }
104
+ }
105
+
106
+ async #load(module, configURL) {
107
+ let result;
108
+ try {
109
+ result = await transport({ url: configURL, method: "GET", timeout: 5000, headers: { Accept: "application/json" } });
110
+ } catch (error) {
111
+ throw Object.assign(new Error(`Configuration request failed: ${error.message}`), { status: 502 });
112
+ }
113
+ const status = result.statusCode ?? result.status;
114
+ if (status !== 200) throw Object.assign(new Error(`Configuration HTTP ${status}`), { status });
115
+ try {
116
+ const body = typeof result.body === "string" ? result.body : new TextDecoder().decode(result.body);
117
+ const boxjs = JSON.parse(body);
118
+ const apps = Array.isArray(boxjs) ? [{ settings: boxjs }] : (boxjs.apps ?? [boxjs]);
119
+ if (!Array.isArray(apps)) throw new TypeError("Expected BoxJS apps array");
120
+ const entries = [];
121
+ let storageKey;
122
+ for (const app of apps) {
123
+ if (!app || !Array.isArray(app.settings)) throw new TypeError("Expected BoxJS settings array");
124
+ for (const entry of app.settings) {
125
+ if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
126
+ if (!entry.id.startsWith("@")) {
127
+ if (Array.isArray(boxjs)) throw new TypeError("BoxJS settings require @root.path IDs");
128
+ continue;
129
+ }
130
+ const [root, ...parts] = entry.id.slice(1).split(".");
131
+ if (!root || root.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
132
+ validatePathParts(parts);
133
+ if (parts[0] !== module) continue;
134
+ if (storageKey && storageKey !== root) throw new TypeError(`A module must use one storage root: ${module}`);
135
+ storageKey = root;
136
+ entries.push(entry);
137
+ }
138
+ }
139
+ if (!entries.length) throw new TypeError(`No BoxJS settings for module: ${module}`);
140
+ return { boxjs, entries, module, storageKey, version: this.#header(result.headers, "x-preferencepanes-version") };
141
+ } catch (error) {
142
+ throw Object.assign(new Error(`Invalid BoxJS: ${error.message}`), { status: 422 });
143
+ }
144
+ }
145
+
146
+ #jsonBody(request) {
147
+ const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
148
+ if (headers["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/json") throw Object.assign(new TypeError("Expected application/json"), { status: 415 });
149
+ if (typeof request.body !== "string" || request.body.length > 65536) throw Object.assign(new TypeError("Expected a JSON body up to 65536 characters"), { status: 400 });
150
+ try {
151
+ return JSON.parse(request.body);
152
+ } catch (error) {
153
+ throw Object.assign(error, { status: 400 });
154
+ }
155
+ }
156
+
157
+ #storagePath(target, key) {
158
+ if (typeof key !== "string") throw Object.assign(new TypeError("A BoxJS field path is required"), { status: 400 });
159
+ const path = `@${target.storageKey}.${key}`;
160
+ if (!target.entries.some(entry => entry.id === path)) throw Object.assign(new TypeError(`Unknown BoxJS field: ${key}`), { status: 400 });
161
+ return path;
162
+ }
163
+
164
+ #scopePath(target, scope) {
165
+ switch (scope) {
166
+ case "settings":
167
+ return `@${target.storageKey}.${target.module}.Settings`;
168
+ case "caches":
169
+ return `@${target.storageKey}.${target.module}.Caches`;
170
+ case "module":
171
+ return `@${target.storageKey}.${target.module}`;
172
+ default:
173
+ throw Object.assign(new TypeError("Scope must be settings, caches or module"), { status: 400 });
174
+ }
175
+ }
176
+
177
+ #response(request, status, body, extraHeaders = {}) {
178
+ return {
179
+ status,
180
+ headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", ...extraHeaders },
181
+ body: request.method === "HEAD" ? "" : JSON.stringify(body),
182
+ };
183
+ }
184
+
185
+ #header(headers, name) {
186
+ const entry = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === name);
187
+ return entry?.[1] === undefined ? undefined : String(entry[1]).trim();
188
+ }
189
+ }
190
+
191
+ new API().run();
@@ -4,24 +4,25 @@
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.
7
8
  * @property {number} [timeout] 超时毫秒数,默认 3500 / Timeout in milliseconds, defaults to 3500.
8
9
  */
9
10
 
10
11
  /**
11
- * 通过模块 JSON Mock 的 HEAD 响应检测安装状态和业务版本。
12
- * Probe installation and business version from the native HEAD response of a module JSON Mock.
13
- * @param {string | URL} url 配置 Mock 地址 / Configuration Mock URL.
12
+ * 通过模块 API 的 HEAD 响应检测安装状态和业务版本。
13
+ * Probe installation and business version from the module API HEAD response.
14
+ * @param {string | URL} url 模块 API 地址 / Module API URL.
14
15
  * @param {ModuleProbeOptions} [options] 请求选项 / Request options.
15
16
  * @returns {Promise<Response>} 原始 HTTP 响应,可直接读取 status 和响应头 / Native HTTP response; read status and headers directly.
16
17
  */
17
- export async function probeModule(url, { fetch: request = globalThis.fetch, signal, timeout = 3500 } = {}) {
18
+ export async function probeModule(url, { fetch: request = globalThis.fetch, json, signal, timeout = 3500 } = {}) {
18
19
  const controller = new AbortController();
19
20
  const abort = () => controller.abort();
20
21
  if (signal?.aborted) abort();
21
22
  signal?.addEventListener("abort", abort, { once: true });
22
23
  const timer = setTimeout(() => controller.abort(), timeout);
23
24
  try {
24
- return await request(url, { method: "HEAD", cache: "no-store", credentials: "omit", signal: controller.signal });
25
+ return await request(url, { method: "HEAD", cache: "no-store", credentials: "omit", signal: controller.signal, headers: json ? { "X-PreferencePanes-JSON": json } : undefined });
25
26
  } finally {
26
27
  clearTimeout(timer);
27
28
  signal?.removeEventListener("abort", abort);
@@ -59,7 +60,7 @@ export class ModuleStatus extends EventTarget {
59
60
  /**
60
61
  * 每次进入重新探测,取消旧请求并忽略其迟到结果。
61
62
  * Reprobe on entry, cancelling old requests and ignoring late results.
62
- * @param {string | URL} url 配置 Mock 地址 / Configuration Mock URL.
63
+ * @param {string | URL} url 模块 API 地址 / Module API URL.
63
64
  * @param {ModuleProbeOptions} [options] 请求选项 / Request options.
64
65
  * @returns {Promise<Response | undefined>} 原始响应;被取消时无返回值 / Native response; undefined when cancelled.
65
66
  */
@@ -169,14 +169,16 @@ 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;
172
174
  /** 超时毫秒数,默认 3500 / Timeout in milliseconds, defaults to 3500. */
173
175
  timeout?: number;
174
176
  }
175
177
 
176
178
  /**
177
- * 通过模块 JSON Mock 的 HEAD 响应检测安装状态和业务版本。
178
- * Probe installation and business version from a module JSON Mock HEAD response.
179
- * @param url 配置 Mock 地址 / Configuration Mock URL.
179
+ * 通过模块 API 的 HEAD 响应检测安装状态和业务版本。
180
+ * Probe installation and business version from a module API HEAD response.
181
+ * @param url 模块 API 地址 / Module API URL.
180
182
  * @param options 请求选项 / Request options.
181
183
  * @returns 原始 HTTP 响应 / Native HTTP response.
182
184
  */
@@ -199,9 +201,9 @@ export class ModuleStatus extends EventTarget {
199
201
  */
200
202
  readonly state: { status: "checking" | "installed" | "missing"; version: string | null };
201
203
  /**
202
- * 探测配置 Mock
203
- * Probe a configuration Mock.
204
- * @param url 配置地址 / Configuration URL.
204
+ * 探测模块 API
205
+ * Probe the module API.
206
+ * @param url 模块 API 地址 / Module API URL.
205
207
  * @param options 请求选项 / Request options.
206
208
  * @returns 原始 HTTP 响应;网络错误时状态行显示“未安装” / Native HTTP response; network errors render “未安装”.
207
209
  */
@@ -1,22 +1,46 @@
1
- import { BoxJS } from "../BoxJS.mjs";
2
- import { validatePathParts } from "./settings-path.mjs";
1
+ import { validatePathParts } from "../lib/settings-path.mjs";
3
2
 
4
3
  /**
5
- * 将 BoxJS 数组、app 或订阅转换为模块字段,保留原文件为唯一字段来源。
6
- * Normalize a BoxJS array, app or subscription using the source JSON as the field authority.
7
- * @param {unknown | BoxJS} config BoxJS JSON 或已解析目录 / BoxJS document or parsed catalog.
8
- * @param {string} module API 第一段模块名 / First API path segment.
9
- * @returns {import("../index.js").ModuleDefinition} 存储根和字段 / Storage root and fields.
10
- * @throws {TypeError} 配置结构、字段路径、默认值或展示属性无效 / Invalid configuration, field path, default or presentation attribute.
4
+ * 将 BoxJS 数组、app 或订阅转换为浏览器字段定义。
5
+ * Normalize a BoxJS array, app or subscription into browser field definitions.
6
+ * @param {unknown} config 原始 BoxJS JSON / Raw BoxJS JSON.
7
+ * @param {string} [module] API 模块路径段;省略时要求输入只有一个模块 / API module path segment; omission requires exactly one module.
8
+ * @returns {import("../index.js").ModuleDefinition} 浏览器字段定义 / Browser field definition.
11
9
  */
12
10
  export function normalizeBoxJs(config, module) {
13
- validatePathParts([module]);
14
- const catalog = config instanceof BoxJS ? config : new BoxJS(config);
15
- const target = catalog.modules.get(module);
11
+ if (!config || typeof config !== "object") throw new TypeError("Expected BoxJS JSON");
12
+ const document = JSON.parse(JSON.stringify(config));
13
+ const apps = Array.isArray(document) ? [{ settings: document }] : (document.apps ?? [document]);
14
+ if (!Array.isArray(apps)) throw new TypeError("Expected BoxJS apps array");
15
+ const modules = new Map();
16
+ for (const app of apps) {
17
+ if (!app || !Array.isArray(app.settings)) throw new TypeError("Expected BoxJS settings array");
18
+ for (const entry of app.settings) {
19
+ if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
20
+ if (!entry.id.startsWith("@")) {
21
+ if (Array.isArray(document)) throw new TypeError("BoxJS settings require @root.path IDs");
22
+ continue;
23
+ }
24
+ const [storageKey, ...parts] = entry.id.slice(1).split(".");
25
+ if (!storageKey || storageKey.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
26
+ validatePathParts(parts);
27
+ const name = parts[0];
28
+ let target = modules.get(name);
29
+ if (!target) {
30
+ target = { module: name, storageKey, entries: [], owners: new Set() };
31
+ modules.set(name, target);
32
+ }
33
+ if (target.storageKey !== storageKey) throw new TypeError(`A module must use one storage root: ${name}`);
34
+ target.entries.push(entry);
35
+ target.owners.add(app);
36
+ }
37
+ }
38
+ if (module === undefined && modules.size !== 1) throw new TypeError("Import BoxJS JSON for exactly one module");
39
+ const target = module === undefined ? modules.values().next().value : modules.get(module);
16
40
  if (!target) throw new TypeError(`No BoxJS settings for module: ${module}`);
17
- const { entries, storageKey, metadata } = target;
41
+ const metadata = normalizeMetadata(target.owners.size === 1 ? presentation([...target.owners][0]) : {});
18
42
  const fields = [];
19
- for (const entry of entries) {
43
+ for (const entry of target.entries) {
20
44
  const parts = entry.id.slice(1).split(".").slice(1);
21
45
  const type = { boolean: "boolean", checkboxes: "array", selects: "select", text: "string", textarea: "string", number: "number" }[entry.type];
22
46
  if (!type) throw new TypeError(`Unsupported BoxJS control: ${entry.type}`);
@@ -46,18 +70,50 @@ export function normalizeBoxJs(config, module) {
46
70
  if (Object.hasOwn(field, "defaultValue") && !validValue(field, field.defaultValue)) throw new TypeError(`Invalid BoxJS val: ${entry.id}`);
47
71
  fields.push(field);
48
72
  }
49
- if (!fields.length) throw new TypeError(`No BoxJS settings for module: ${module}`);
73
+ if (!fields.length) throw new TypeError(`No BoxJS settings for module: ${target.module}`);
50
74
  const common = fields[0].key.split(".").slice(0, -1);
51
75
  for (const field of fields) while (!field.key.startsWith(`${common.join(".")}.`)) common.pop();
52
76
  return {
53
- module,
54
- storageKey,
77
+ module: target.module,
78
+ storageKey: target.storageKey,
55
79
  fields,
56
80
  settingsPath: common,
57
81
  ...(Object.keys(metadata).length ? { metadata } : {}),
58
82
  };
59
83
  }
60
84
 
85
+ /**
86
+ * 保留字段所属 app 的原始展示信息。
87
+ * Retain raw presentation metadata from the app owning the fields.
88
+ * @param {object} source BoxJS app / BoxJS app.
89
+ * @returns {Record<string, unknown>} 原始展示信息 / Raw presentation metadata.
90
+ */
91
+ function presentation(source) {
92
+ const result = {};
93
+ for (const key of ["id", "name", "author", "repo", "script", "icon", "description", "desc", "icons", "descs"]) {
94
+ if (source[key] === undefined) continue;
95
+ result[key] = source[key];
96
+ }
97
+ return result;
98
+ }
99
+
100
+ /**
101
+ * 校验供浏览器展示的标准 BoxJS 元数据。
102
+ * Validate standard BoxJS metadata used by the browser renderer.
103
+ * @param {Record<string, unknown>} source 原始展示元数据 / Raw presentation metadata.
104
+ * @returns {import("../index.js").BoxJSMetadata} 规范化展示元数据 / Normalized presentation metadata.
105
+ */
106
+ function normalizeMetadata(source) {
107
+ const result = {};
108
+ for (const [key, value] of Object.entries(source)) {
109
+ const multiple = key === "icons" || key === "descs";
110
+ const values = multiple ? value : [value];
111
+ if (!Array.isArray(values) || values.some(item => typeof item !== "string")) throw new TypeError(`Invalid BoxJS app ${key}`);
112
+ result[key] = multiple ? [...values] : value;
113
+ }
114
+ return result;
115
+ }
116
+
61
117
  /**
62
118
  * 归一化 BoxJS 的字符串存储值,不改变普通文本内容。
63
119
  * Normalize BoxJS string persistence without changing free-text values.
@@ -1,157 +1,67 @@
1
- import type { ModuleDefinition, SettingsScalar } from "../index.js";
1
+ import type { ModuleDefinition, ModuleModel, SettingsScalar } from "../index.js";
2
2
  /**
3
- * 单键写入或删除的通知事件,携带对应模块与点分键路径。
4
- * Notification for a single-key write or delete, including module and dotted key path.
3
+ * 单键写入或删除的通知事件。
4
+ * Notification for a single-key write or delete.
5
5
  */
6
6
  export interface Notification {
7
- /**
8
- * 结果类别
9
- * Result kind.
10
- */
7
+ /** 结果类别 / Result kind. */
11
8
  kind: "success" | "error";
12
- /**
13
- * 操作类别
14
- * Operation kind.
15
- */
9
+ /** 操作类别 / Operation kind. */
16
10
  operation: "write" | "delete" | "clearCaches" | "reset";
17
- /**
18
- * 模块标识
19
- * Module identifier.
20
- */
11
+ /** 模块标识 / Module identifier. */
21
12
  module: string;
22
- /**
23
- * 含模块名、不含存储根的点分路径
24
- * Dotted path including the module but excluding the storage root.
25
- */
26
- key: string;
27
- /**
28
- * 失败原因,仅错误事件提供
29
- * Failure reason, provided for errors only.
30
- */
13
+ /** 字段路径 / Field path. */
14
+ key?: string;
15
+ /** 失败原因 / Failure reason. */
31
16
  message?: string;
32
17
  }
33
18
  /**
34
- * 浏览器会话客户端选项。
35
- * Options for the browser session client.
19
+ * 浏览器页面客户端选项;模型由 API 提供。
20
+ * Browser page client options; the model is supplied by the API.
36
21
  */
37
22
  export interface PreferencesClientOptions {
38
- /**
39
- * 包内从 BoxJS 推导的目录。
40
- * Internal catalog derived from BoxJS.
41
- */
42
- catalog: { modules: ReadonlyMap<string, { storageKey: string }>; select(module: string): unknown };
43
- /**
44
- * 默认使用浏览器 fetch,可注入同签名传输
45
- * Defaults to browser fetch; an equivalent transport may be supplied.
46
- */
23
+ /** API 返回的模块模型 / Module model returned by the API. */
24
+ model: ModuleModel;
25
+ /** 用于渲染的归一化字段定义 / Normalized field definition for rendering. */
26
+ definition: ModuleDefinition;
27
+ /** 默认使用浏览器 fetch / Defaults to browser fetch. */
47
28
  fetch?: typeof globalThis.fetch;
48
- /**
49
- * 成功写入或失败时调用,不用于读取事件
50
- * Called for successful mutations or failures, not reads.
51
- */
29
+ /** 操作通知 / Mutation notifications. */
52
30
  notify?: (notification: Notification) => void;
53
- /**
54
- * 单次请求超时,单位毫秒,默认 10000
55
- * Per-request timeout in milliseconds; defaults to 10000.
56
- */
31
+ /** 请求超时毫秒数 / Request timeout in milliseconds. */
57
32
  timeout?: number;
58
33
  }
59
34
  /**
60
- * 会话的深拷贝快照,调用方修改不会影响缓存。
61
- * Deep-cloned session snapshot; caller changes cannot alter the cache.
35
+ * 会话的深拷贝快照。
36
+ * Deep-cloned session snapshot.
62
37
  */
63
38
  export interface ModuleSnapshot {
64
- /**
65
- * 当前配置生成的模块定义
66
- * Module definition generated from current config.
67
- */
39
+ /** 当前模块字段定义 / Current module field definition. */
68
40
  definition: ModuleDefinition;
69
- /**
70
- * 点分键到显示值的映射,已包含适用的默认值;持久化 null 原样保留。
71
- * Dotted keys mapped to display values including applicable defaults; persisted null is preserved.
72
- */
41
+ /** 当前页面值 / Current page values. */
73
42
  values: Record<string, SettingsScalar | SettingsScalar[] | null>;
74
43
  }
75
44
  /**
76
- * 只在页面存活期间维护模块缓存的通用客户端。
77
- * Generic client maintaining module caches only during the page lifetime.
45
+ * 管理单模块页面的 API 请求、值快照和会话终止。
46
+ * Manage API requests, value snapshots, and session termination for one module page.
78
47
  */
79
- export interface PreferencesClient {
80
- /**
81
- * 使用已导入的 JSON 替换会话,只读取一次设置值。
82
- * Replace the session from imported JSON and read stored settings once.
83
- * @param module 模块标识 / Module identifier.
84
- * @returns 新会话的独立快照 / Independent snapshot of the new session.
85
- * @throws {Error} 写入进行中、请求或配置无效、会话被替换 / Active write, invalid request or config, or replaced session.
86
- */
87
- open(module: string): Promise<ModuleSnapshot>;
88
- /**
89
- * 获取已打开模块的快照,不发请求。
90
- * Get a snapshot of an open module without network requests.
91
- * @param module 模块标识 / Module identifier.
92
- * @returns 深拷贝快照 / Deep-cloned snapshot.
93
- * @throws {Error} 模块尚未打开 / Module has not been opened.
94
- */
95
- snapshot(module: string): ModuleSnapshot;
96
- /**
97
- * 取消未完成的读取并移除缓存,不撤销已发送的写入。
98
- * Abort pending reads and discard the cache without undoing dispatched writes.
99
- * @param module 模块标识 / Module identifier.
100
- * @returns 无返回值 / No return value.
101
- */
102
- leave(module: string): void;
103
- /**
104
- * POST 单个字段,HTTP 200 后更新缓存,不追加 GET。
105
- * POST one field and update its cache only on HTTP 200, without a follow-up GET.
106
- * @param module 已打开的模块 / Open module.
107
- * @param key 完整点分字段路径 / Complete dotted field path.
108
- * @param value 符合字段类型和选项的值 / Value matching the field type and choices.
109
- * @returns 操作完成 / Completion of the operation.
110
- * @throws {Error} 会话、值、并发写入或网络错误 / Session, value, concurrent-write or network error.
111
- */
112
- set(module: string, key: string, value: SettingsScalar | SettingsScalar[]): Promise<void>;
113
- /**
114
- * POST /api/delete 删除覆盖值,200 后显示默认值,不追加读取。
115
- * POST /api/delete removes an override and displays its default after 200, without rereading.
116
- * @param module 已打开的模块 / Open module.
117
- * @param key 完整点分字段路径 / Complete dotted field path.
118
- * @returns 操作完成 / Completion of the operation.
119
- * @throws {Error} 会话、路径、并发写入或网络错误 / Session, path, concurrent-write or network error.
120
- */
121
- remove(module: string, key: string): Promise<void>;
122
- /**
123
- * 按需重新读取整个模块 Settings,不更新页面会话缓存。
124
- * Reread all module Settings on demand without updating the page-session cache.
125
- * @param module 已打开模块 / Open module.
126
- * @returns 设置 JSON 值,缺失时为 undefined / Settings JSON value, or undefined when absent.
127
- */
128
- readSettings(module: string): Promise<unknown>;
129
- /**
130
- * 按需读取整个模块 Caches,不刷新设置。
131
- * Read all module Caches on demand without refreshing settings.
132
- * @param module 已打开模块 / Open module.
133
- * @returns 缓存 JSON 值,缺失时为 undefined / Cache JSON value, or undefined when absent.
134
- */
135
- readCaches(module: string): Promise<unknown>;
136
- /**
137
- * 删除模块 Caches 并更新相关页面状态,不追加 GET。
138
- * Delete module Caches and update related page state without a follow-up GET.
139
- * @param module 已打开模块 / Open module.
140
- * @returns 清理完成 / Cleanup completion.
141
- */
142
- clearCaches(module: string): Promise<void>;
143
- /**
144
- * 删除整个模块持久化数据,页面使用当前 BoxJS 默认值。
145
- * Delete all module persistence and use current BoxJS defaults on the page.
146
- * @param module 已打开模块 / Open module.
147
- * @returns 重置完成 / Reset completion.
148
- */
149
- reset(module: string): Promise<void>;
48
+ export class PreferencesClient {
49
+ /** 创建页面客户端 / Create the page client. */
50
+ constructor(options: PreferencesClientOptions);
51
+ /** 获取页面快照,不发请求 / Get a page snapshot without a request. */
52
+ snapshot(): ModuleSnapshot;
53
+ /** 读取 Settings 子树 / Read the Settings subtree. */
54
+ readSettings(): Promise<unknown>;
55
+ /** 读取 Caches 子树 / Read the Caches subtree. */
56
+ readCaches(): Promise<unknown>;
57
+ /** 删除 Caches / Delete Caches. */
58
+ clearCaches(): Promise<void>;
59
+ /** 删除整个模块数据并恢复默认值 / Delete module data and restore defaults. */
60
+ reset(): Promise<void>;
61
+ /** 取消请求 / Cancel requests. */
62
+ leave(): void;
63
+ /** 写入单个字段 / Write one field. */
64
+ set(key: string, value: SettingsScalar | SettingsScalar[]): Promise<void>;
65
+ /** 删除单个字段覆盖值 / Delete one field override. */
66
+ remove(key: string): Promise<void>;
150
67
  }
151
- /**
152
- * 创建包内客户端,接管请求和会话。
153
- * Create an internal client for requests and sessions.
154
- * @param options 包内目录与运行环境 / Internal catalog and runtime environment.
155
- * @returns 会话客户端 / Session client.
156
- */
157
- export function createPreferencesClient(options: PreferencesClientOptions): PreferencesClient;