@nsnanocat/preference-panes 0.5.0 → 0.7.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/README.md +31 -124
- package/dist/module/app.mjs +1030 -0
- package/dist/module/index.html +14 -0
- package/dist/preference-panes.config.js +842 -0
- package/dist/preference-panes.mjs +935 -825
- package/dist/preference-panes.proxy.js +1726 -2121
- package/package.json +62 -66
- package/src/BoxJS.mjs +86 -0
- package/src/Store.mjs +127 -0
- package/src/browser/app.mjs +24 -148
- package/src/browser/client.d.mts +150 -0
- package/src/browser/client.mjs +191 -212
- package/src/browser/components.mjs +59 -0
- package/src/browser/index.d.ts +19 -152
- package/src/browser/index.mjs +60 -5
- package/src/browser/module.html +14 -0
- package/src/browser/panel.css +221 -221
- package/src/browser/panel.mjs +455 -514
- package/src/build.mjs +31 -0
- package/src/index.d.ts +227 -133
- package/src/index.mjs +3 -6
- package/src/lib/boxjs.mjs +77 -99
- package/src/lib/response.mjs +16 -0
- package/src/lib/settings-path.mjs +10 -23
- package/src/proxy/config.mjs +14 -0
- package/src/proxy/handler.mjs +40 -27
- package/src/proxy/response.mjs +16 -0
- package/dist/preference-panes.request.js +0 -2124
- package/dist/settings/app.mjs +0 -1044
- package/dist/settings/home.css +0 -134
- package/dist/settings/index.html +0 -15
- package/dist/settings/panel.css +0 -325
- package/src/PreferencesHandler.mjs +0 -50
- package/src/SettingsHandler.mjs +0 -142
- package/src/browser/home.css +0 -134
- package/src/browser/site.html +0 -15
- package/src/proxy/request.mjs +0 -5
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ModuleDefinition, SettingsScalar } from "../index.js";
|
|
2
|
+
/**
|
|
3
|
+
* 单键写入或删除的通知事件,携带对应模块与点分键路径。
|
|
4
|
+
* Notification for a single-key write or delete, including module and dotted key path.
|
|
5
|
+
*/
|
|
6
|
+
export interface Notification {
|
|
7
|
+
/**
|
|
8
|
+
* 结果类别
|
|
9
|
+
* Result kind.
|
|
10
|
+
*/
|
|
11
|
+
kind: "success" | "error";
|
|
12
|
+
/**
|
|
13
|
+
* 操作类别
|
|
14
|
+
* Operation kind.
|
|
15
|
+
*/
|
|
16
|
+
operation: "write" | "delete" | "clearCaches" | "reset";
|
|
17
|
+
/**
|
|
18
|
+
* 模块标识
|
|
19
|
+
* Module identifier.
|
|
20
|
+
*/
|
|
21
|
+
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
|
+
*/
|
|
31
|
+
message?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 浏览器会话客户端选项。
|
|
35
|
+
* Options for the browser session client.
|
|
36
|
+
*/
|
|
37
|
+
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
|
+
*/
|
|
47
|
+
fetch?: typeof globalThis.fetch;
|
|
48
|
+
/**
|
|
49
|
+
* 成功写入或失败时调用,不用于读取事件
|
|
50
|
+
* Called for successful mutations or failures, not reads.
|
|
51
|
+
*/
|
|
52
|
+
notify?: (notification: Notification) => void;
|
|
53
|
+
/**
|
|
54
|
+
* 单次请求超时,单位毫秒,默认 10000
|
|
55
|
+
* Per-request timeout in milliseconds; defaults to 10000.
|
|
56
|
+
*/
|
|
57
|
+
timeout?: number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 会话的深拷贝快照,调用方修改不会影响缓存。
|
|
61
|
+
* Deep-cloned session snapshot; caller changes cannot alter the cache.
|
|
62
|
+
*/
|
|
63
|
+
export interface ModuleSnapshot {
|
|
64
|
+
/**
|
|
65
|
+
* 当前配置生成的模块定义
|
|
66
|
+
* Module definition generated from current config.
|
|
67
|
+
*/
|
|
68
|
+
definition: ModuleDefinition;
|
|
69
|
+
/**
|
|
70
|
+
* 点分键到显示值的映射,已包含适用的默认值;持久化 null 原样保留。
|
|
71
|
+
* Dotted keys mapped to display values including applicable defaults; persisted null is preserved.
|
|
72
|
+
*/
|
|
73
|
+
values: Record<string, SettingsScalar | SettingsScalar[] | null>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* 只在页面存活期间维护模块缓存的通用客户端。
|
|
77
|
+
* Generic client maintaining module caches only during the page lifetime.
|
|
78
|
+
*/
|
|
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
|
+
* DELETE 单个覆盖值,HTTP 200 后显示默认值,不追加 GET。
|
|
115
|
+
* DELETE an override and display its default after HTTP 200, without a follow-up GET.
|
|
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
|
+
* 按需读取整个模块 Caches,不刷新设置。
|
|
124
|
+
* Read all module Caches on demand without refreshing settings.
|
|
125
|
+
* @param module 已打开模块 / Open module.
|
|
126
|
+
* @returns 缓存 JSON 值,缺失时为 undefined / Cache JSON value, or undefined when absent.
|
|
127
|
+
*/
|
|
128
|
+
readCaches(module: string): Promise<unknown>;
|
|
129
|
+
/**
|
|
130
|
+
* 删除模块 Caches 并更新相关页面状态,不追加 GET。
|
|
131
|
+
* Delete module Caches and update related page state without a follow-up GET.
|
|
132
|
+
* @param module 已打开模块 / Open module.
|
|
133
|
+
* @returns 清理完成 / Cleanup completion.
|
|
134
|
+
*/
|
|
135
|
+
clearCaches(module: string): Promise<void>;
|
|
136
|
+
/**
|
|
137
|
+
* 删除整个模块持久化数据,页面使用当前 BoxJS 默认值。
|
|
138
|
+
* Delete all module persistence and use current BoxJS defaults on the page.
|
|
139
|
+
* @param module 已打开模块 / Open module.
|
|
140
|
+
* @returns 重置完成 / Reset completion.
|
|
141
|
+
*/
|
|
142
|
+
reset(module: string): Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 创建包内客户端,接管请求和会话。
|
|
146
|
+
* Create an internal client for requests and sessions.
|
|
147
|
+
* @param options 包内目录与运行环境 / Internal catalog and runtime environment.
|
|
148
|
+
* @returns 会话客户端 / Session client.
|
|
149
|
+
*/
|
|
150
|
+
export function createPreferencesClient(options: PreferencesClientOptions): PreferencesClient;
|
package/src/browser/client.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { normalizeBoxJs, normalizeStoredValue, validValue } from "../lib/boxjs.mjs";
|
|
2
|
-
import { validatePathParts } from "../lib/settings-path.mjs";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* 单个模块的临时会话;离开页面后丢弃。
|
|
@@ -7,222 +6,202 @@ import { validatePathParts } from "../lib/settings-path.mjs";
|
|
|
7
6
|
* @typedef {object} ModuleSession
|
|
8
7
|
* @property {AbortController} controller 读取请求的取消控制器 / Abort controller for reads.
|
|
9
8
|
* @property {import("../index.js").ModuleDefinition | null} definition 加载完成的配置,加载中为 null / Loaded configuration, or null while loading.
|
|
10
|
-
* @property {import("./
|
|
9
|
+
* @property {import("./client.mjs").ModuleSnapshot["values"]} values 当前显示值 / Current display values.
|
|
11
10
|
* @property {boolean} saving 是否正在写入 / Whether a mutation is in progress.
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* 创建页面会话缓存;打开时重读,选项操作仅在 HTTP 200 后更新缓存。
|
|
16
15
|
* Create a page-session cache; reload on open and mutate cache only after HTTP 200.
|
|
17
|
-
* @param {import("./
|
|
18
|
-
* @returns {import("./
|
|
16
|
+
* @param {import("./client.mjs").PreferencesClientOptions} options 包内目录、请求与通知 / Internal catalog, requests and notifications.
|
|
17
|
+
* @returns {import("./client.mjs").PreferencesClient} 通用客户端 / Generic client.
|
|
19
18
|
*/
|
|
20
|
-
export function createPreferencesClient({ fetch: request = globalThis.fetch.bind(globalThis), notify = () => {}, timeout = 10000 }
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
sessions.delete(module);
|
|
209
|
-
},
|
|
210
|
-
/**
|
|
211
|
-
* 写入单键并更新当前会话。
|
|
212
|
-
* Write one key and update the current session.
|
|
213
|
-
* @param {string} module 已打开模块 / Open module.
|
|
214
|
-
* @param {string} key 点分字段路径 / Dotted field path.
|
|
215
|
-
* @param {import("../index.js").SettingsScalar | import("../index.js").SettingsScalar[]} value 字段值 / Field value.
|
|
216
|
-
* @returns {Promise<void>} 写入完成 / Write completion.
|
|
217
|
-
*/
|
|
218
|
-
set: (module, key, value) => change(module, key, "POST", value),
|
|
219
|
-
/**
|
|
220
|
-
* 删除单键覆盖值并显示默认值。
|
|
221
|
-
* Delete one override and display its default value.
|
|
222
|
-
* @param {string} module 已打开模块 / Open module.
|
|
223
|
-
* @param {string} key 点分字段路径 / Dotted field path.
|
|
224
|
-
* @returns {Promise<void>} 删除完成 / Delete completion.
|
|
225
|
-
*/
|
|
226
|
-
remove: (module, key) => change(module, key, "DELETE"),
|
|
227
|
-
};
|
|
19
|
+
export function createPreferencesClient({ catalog, fetch: request = globalThis.fetch.bind(globalThis), notify = () => {}, timeout = 10000 }) {
|
|
20
|
+
/**
|
|
21
|
+
* 模块会话表
|
|
22
|
+
* Module session map.
|
|
23
|
+
* @type {Map<string, ModuleSession>}
|
|
24
|
+
*/
|
|
25
|
+
const sessions = new Map();
|
|
26
|
+
/**
|
|
27
|
+
* 发送同源请求,处理超时与取消;数据 GET 的 404 交给调用方处理。
|
|
28
|
+
* Send a same-origin request with timeout and cancellation; callers handle missing-data GET responses.
|
|
29
|
+
* @param {string} path 相对请求路径 / Relative request path.
|
|
30
|
+
* @param {"HEAD" | "GET" | "POST" | "DELETE"} method HTTP 方法 / HTTP method.
|
|
31
|
+
* @param {unknown} body POST 值,其它方法忽略 / POST value, ignored by other methods.
|
|
32
|
+
* @param {AbortSignal | undefined} signal 会话取消信号 / Session cancellation signal.
|
|
33
|
+
* @returns {Promise<Response>} 未消费正文的响应 / Response with an unread body.
|
|
34
|
+
* @throws {Error} 非 200 且非数据 GET 404、超时、取消或网络错误 / Non-200 status except missing-data GETs, timeout, cancellation or network error.
|
|
35
|
+
*/
|
|
36
|
+
async function send(path, method, body, signal) {
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const abort = () => controller.abort();
|
|
39
|
+
if (signal?.aborted) abort();
|
|
40
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
41
|
+
const timer = setTimeout(abort, timeout);
|
|
42
|
+
try {
|
|
43
|
+
const response = await request(path, {
|
|
44
|
+
method,
|
|
45
|
+
credentials: "omit",
|
|
46
|
+
cache: "no-store",
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
headers: { "X-Settings-Client": "1", ...(method === "POST" ? { "Content-Type": "application/json" } : {}) },
|
|
49
|
+
...(method === "POST" ? { body: JSON.stringify(body) } : {}),
|
|
50
|
+
});
|
|
51
|
+
if (response.status !== 200 && !(method === "GET" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
|
|
52
|
+
return response;
|
|
53
|
+
} finally {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
signal?.removeEventListener("abort", abort);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 获取独立快照,避免调用方修改内部缓存。
|
|
60
|
+
* Return an independent snapshot so callers cannot mutate the cache.
|
|
61
|
+
* @param {string} module 已打开模块 / Open module.
|
|
62
|
+
* @returns {import("./client.mjs").ModuleSnapshot} 会话快照 / Session snapshot.
|
|
63
|
+
* @throws {Error} 模块未完成加载 / Module has not finished loading.
|
|
64
|
+
*/
|
|
65
|
+
const snapshot = module => {
|
|
66
|
+
const state = sessions.get(module);
|
|
67
|
+
if (!state?.definition) throw new Error("Open the module first");
|
|
68
|
+
return structuredClone({ definition: state.definition, values: state.values });
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* 串行修改单键,仅成功后更新仍存活的会话。
|
|
72
|
+
* Serialize single-key mutations and update a still-active session only after success.
|
|
73
|
+
* @param {string} module 已打开模块 / Open module.
|
|
74
|
+
* @param {string} key 完整点分字段路径 / Complete dotted field path.
|
|
75
|
+
* @param {"POST" | "DELETE"} method 写入或删除 / Write or delete.
|
|
76
|
+
* @param {unknown} value 写入值,删除时忽略 / Write value, ignored for deletion.
|
|
77
|
+
* @param {"write" | "delete" | "clearCaches" | "reset"} [operation] 操作类型 / Operation kind.
|
|
78
|
+
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
79
|
+
* @throws {Error} 会话、字段、值或请求错误 / Session, field, value or request error.
|
|
80
|
+
*/
|
|
81
|
+
async function change(module, key, method, value, operation = method === "POST" ? "write" : "delete") {
|
|
82
|
+
const state = sessions.get(module);
|
|
83
|
+
if (!state?.definition) throw new Error("Open the module first");
|
|
84
|
+
if (state.saving) throw new Error("A settings write is already in progress");
|
|
85
|
+
const field = state.definition.fields.find(field => field.key === key);
|
|
86
|
+
state.saving = true;
|
|
87
|
+
try {
|
|
88
|
+
if ((operation === "write" || operation === "delete") && (!field || (method === "POST" && !validValue(field, value)))) throw new TypeError("Invalid setting value");
|
|
89
|
+
await send(`/api/${key.split(".").map(encodeURIComponent).join("/")}`, method, value);
|
|
90
|
+
if (sessions.get(module) === state) {
|
|
91
|
+
switch (operation) {
|
|
92
|
+
case "write":
|
|
93
|
+
state.values[key] = structuredClone(value);
|
|
94
|
+
break;
|
|
95
|
+
case "delete":
|
|
96
|
+
case "clearCaches":
|
|
97
|
+
case "reset":
|
|
98
|
+
for (const candidate of state.definition.fields) {
|
|
99
|
+
if (candidate.key !== key && !candidate.key.startsWith(`${key}.`)) continue;
|
|
100
|
+
delete state.values[candidate.key];
|
|
101
|
+
if (Object.hasOwn(candidate, "defaultValue")) state.values[candidate.key] = structuredClone(candidate.defaultValue);
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
notify({ kind: "success", operation, module, key });
|
|
107
|
+
} catch (error) {
|
|
108
|
+
notify({ kind: "error", operation, module, key, message: error.message });
|
|
109
|
+
throw error;
|
|
110
|
+
} finally {
|
|
111
|
+
state.saving = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
/**
|
|
116
|
+
* 从已导入的 JSON 创建新会话,只读取一次设置值。
|
|
117
|
+
* Create a session from imported JSON and read stored settings once.
|
|
118
|
+
* @param {string} module 模块标识 / Module identifier.
|
|
119
|
+
* @returns {Promise<import("./client.mjs").ModuleSnapshot>} 新快照 / New snapshot.
|
|
120
|
+
* @throws {Error} 读取失败、会话被替换或写入尚未完成 / Read failure, replaced session or unfinished write.
|
|
121
|
+
*/
|
|
122
|
+
async open(module) {
|
|
123
|
+
const binding = catalog.modules.get(module);
|
|
124
|
+
if (!binding) throw new TypeError(`No BoxJS settings for module: ${module}`);
|
|
125
|
+
const previous = sessions.get(module);
|
|
126
|
+
if (previous?.saving) throw new Error("Cannot refresh while saving");
|
|
127
|
+
previous?.controller.abort();
|
|
128
|
+
const state = { controller: new AbortController(), definition: null, values: {}, saving: false };
|
|
129
|
+
sessions.set(module, state);
|
|
130
|
+
try {
|
|
131
|
+
const definition = normalizeBoxJs(catalog.select(module), module);
|
|
132
|
+
const response = await send(`/api/${definition.settingsPath.map(encodeURIComponent).join("/")}/`, "GET", undefined, state.controller.signal);
|
|
133
|
+
let subtree = response.status === 404 ? {} : await response.json();
|
|
134
|
+
if (typeof subtree === "string") subtree = JSON.parse(subtree);
|
|
135
|
+
if (!subtree || typeof subtree !== "object" || Array.isArray(subtree)) throw new TypeError("Expected a settings subtree object");
|
|
136
|
+
if (sessions.get(module) !== state) throw new Error("Module session was replaced");
|
|
137
|
+
state.definition = definition;
|
|
138
|
+
for (const field of definition.fields) {
|
|
139
|
+
const stored = field.key
|
|
140
|
+
.split(".")
|
|
141
|
+
.slice(definition.settingsPath.length)
|
|
142
|
+
.reduce((parent, part) => Object(parent)[part], subtree);
|
|
143
|
+
const value = stored === undefined ? field.defaultValue : stored;
|
|
144
|
+
if (value !== undefined) state.values[field.key] = normalizeStoredValue(field, value);
|
|
145
|
+
}
|
|
146
|
+
return snapshot(module);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (sessions.get(module) === state) sessions.delete(module);
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
snapshot,
|
|
153
|
+
/**
|
|
154
|
+
* 按需读取模块 Caches,不自动读取其它设置。
|
|
155
|
+
* Read module Caches on demand without refreshing other settings.
|
|
156
|
+
* @param {string} module 已打开的模块 / Open module.
|
|
157
|
+
* @returns {Promise<unknown>} 缓存值,缺失为 undefined / Cache value, or undefined when absent.
|
|
158
|
+
*/
|
|
159
|
+
async readCaches(module) {
|
|
160
|
+
const state = sessions.get(module);
|
|
161
|
+
if (!state?.definition) throw new Error("Open the module first");
|
|
162
|
+
const response = await send(`/api/${encodeURIComponent(module)}/Caches`, "GET", undefined, state.controller.signal);
|
|
163
|
+
return response.status === 404 ? undefined : response.json();
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* 删除整个 Caches 节点,成功后不追加 GET。
|
|
167
|
+
* Delete the entire Caches node without a follow-up GET.
|
|
168
|
+
* @param {string} module 已打开模块 / Open module.
|
|
169
|
+
* @returns {Promise<void>} 清理完成 / Cleanup completion.
|
|
170
|
+
*/
|
|
171
|
+
clearCaches: module => change(module, `${module}.Caches`, "DELETE", undefined, "clearCaches"),
|
|
172
|
+
/**
|
|
173
|
+
* 删除整个模块持久化节点,以当前 BoxJS 默认值重置页面缓存。
|
|
174
|
+
* Delete module persistence and reset the page cache using current BoxJS defaults.
|
|
175
|
+
* @param {string} module 已打开模块 / Open module.
|
|
176
|
+
* @returns {Promise<void>} 重置完成 / Reset completion.
|
|
177
|
+
*/
|
|
178
|
+
reset: module => change(module, module, "DELETE", undefined, "reset"),
|
|
179
|
+
/**
|
|
180
|
+
* 取消读取并清除会话,不撤销已发送的写入。
|
|
181
|
+
* Abort reads and clear the session without undoing dispatched writes.
|
|
182
|
+
* @param {string} module 模块标识 / Module identifier.
|
|
183
|
+
* @returns {void} 无返回值 / No return value.
|
|
184
|
+
*/
|
|
185
|
+
leave(module) {
|
|
186
|
+
sessions.get(module)?.controller.abort();
|
|
187
|
+
sessions.delete(module);
|
|
188
|
+
},
|
|
189
|
+
/**
|
|
190
|
+
* 写入单键并更新当前会话。
|
|
191
|
+
* Write one key and update the current session.
|
|
192
|
+
* @param {string} module 已打开模块 / Open module.
|
|
193
|
+
* @param {string} key 点分字段路径 / Dotted field path.
|
|
194
|
+
* @param {import("../index.js").SettingsScalar | import("../index.js").SettingsScalar[]} value 字段值 / Field value.
|
|
195
|
+
* @returns {Promise<void>} 写入完成 / Write completion.
|
|
196
|
+
*/
|
|
197
|
+
set: (module, key, value) => change(module, key, "POST", value),
|
|
198
|
+
/**
|
|
199
|
+
* 删除单键覆盖值并显示默认值。
|
|
200
|
+
* Delete one override and display its default value.
|
|
201
|
+
* @param {string} module 已打开模块 / Open module.
|
|
202
|
+
* @param {string} key 点分字段路径 / Dotted field path.
|
|
203
|
+
* @returns {Promise<void>} 删除完成 / Delete completion.
|
|
204
|
+
*/
|
|
205
|
+
remove: (module, key) => change(module, key, "DELETE"),
|
|
206
|
+
};
|
|
228
207
|
}
|