@qilin-agent/client-ui-settings 0.0.0-stage → 3.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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +58 -0
- package/README.md +112 -2
- package/README.zh.md +113 -0
- package/lib/client.js +1545 -0
- package/lib/index.js +20 -0
- package/lib/types/client/config-form-types.d.ts +75 -0
- package/lib/types/client/config-form.d.ts +159 -0
- package/lib/types/client/contract/slots.d.ts +155 -0
- package/lib/types/client/developer-tools.d.ts +22 -0
- package/lib/types/client/index.d.ts +36 -0
- package/lib/types/client/schema.d.ts +72 -0
- package/lib/types/client/settings-mirror.d.ts +118 -0
- package/lib/types/developer-tools-settings.d.ts +16 -0
- package/lib/types/index.d.ts +6 -0
- package/package.json +63 -3
package/lib/client.js
ADDED
|
@@ -0,0 +1,1545 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@qilin-agent/client-ui-settings",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let _qilin_agent_kylin = require("@qilin-agent/kylin");
|
|
8
|
+
let _qilin_agent_client_store = require("@qilin-agent/client-store");
|
|
9
|
+
//#region ../../../vendor/cosmokit/src/misc.ts
|
|
10
|
+
/** Return true when a value is `null` or `undefined`. */
|
|
11
|
+
function isNullable(value) {
|
|
12
|
+
return value === null || value === void 0;
|
|
13
|
+
}
|
|
14
|
+
/** Return true for non-array object values. */
|
|
15
|
+
function isPlainObject(data) {
|
|
16
|
+
return data && typeof data === "object" && !Array.isArray(data);
|
|
17
|
+
}
|
|
18
|
+
/** Filter object entries and return a new object. */
|
|
19
|
+
function filterKeys(object, filter) {
|
|
20
|
+
return Object.fromEntries(Object.entries(object).filter(([key, value]) => filter(key, value)));
|
|
21
|
+
}
|
|
22
|
+
/** Map object values while preserving the original key set. */
|
|
23
|
+
function mapValues(object, transform) {
|
|
24
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key, transform(value, key)]));
|
|
25
|
+
}
|
|
26
|
+
/** Pick selected keys from an object, optionally including `undefined` values. */
|
|
27
|
+
function pick(source, keys, forced) {
|
|
28
|
+
if (!keys) return { ...source };
|
|
29
|
+
const result = {};
|
|
30
|
+
for (const key of keys) if (forced || source[key] !== void 0) result[key] = source[key];
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region ../../../vendor/cosmokit/src/volatile.ts
|
|
35
|
+
/** Shared config references used by schema validators and plugin runtimes. */
|
|
36
|
+
const write = Symbol.for("cosmokit.volatile.write");
|
|
37
|
+
function snapshot(value, ancestors = /* @__PURE__ */ new Set()) {
|
|
38
|
+
if (typeof value === "function") throw new TypeError("volatile config cannot contain functions");
|
|
39
|
+
if (value === null || typeof value !== "object") return value;
|
|
40
|
+
if (ancestors.has(value)) throw new TypeError("volatile config cannot contain cycles");
|
|
41
|
+
ancestors.add(value);
|
|
42
|
+
try {
|
|
43
|
+
if (Array.isArray(value)) return Object.freeze(value.map((item) => snapshot(item, ancestors)));
|
|
44
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new TypeError("volatile config objects must be plain objects or arrays");
|
|
45
|
+
return Object.freeze(Object.fromEntries(Object.entries(value).map(([key, item]) => [key, snapshot(item, ancestors)])));
|
|
46
|
+
} finally {
|
|
47
|
+
ancestors.delete(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a detached reference containing an immutable copy of the supplied data.
|
|
52
|
+
* @param value - validated config data; class instances and functions are unsupported.
|
|
53
|
+
* @returns a reference whose value is updated only by its owning runtime.
|
|
54
|
+
*/
|
|
55
|
+
function createVolatile(value) {
|
|
56
|
+
let current = snapshot(value);
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
get: () => current,
|
|
59
|
+
[write]: (value) => {
|
|
60
|
+
current = value;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Identify references across ESM/CJS copies of the shared library.
|
|
66
|
+
* @param value - a parsed config value.
|
|
67
|
+
* @returns whether the value implements the shared reference protocol.
|
|
68
|
+
*/
|
|
69
|
+
function isVolatile(value) {
|
|
70
|
+
return typeof value === "object" && value !== null && write in value;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region ../../../vendor/cosmokit/src/types.ts
|
|
74
|
+
/** Test values using `instanceof` with a `toStringTag` fallback. */
|
|
75
|
+
function is(type, value) {
|
|
76
|
+
if (arguments.length === 1) return (value) => is(type, value);
|
|
77
|
+
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
78
|
+
}
|
|
79
|
+
function isArrayBufferLike(value) {
|
|
80
|
+
return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
|
|
81
|
+
}
|
|
82
|
+
function isArrayBufferSource(value) {
|
|
83
|
+
return isArrayBufferLike(value) || ArrayBuffer.isView(value);
|
|
84
|
+
}
|
|
85
|
+
let Binary;
|
|
86
|
+
(function(_Binary) {
|
|
87
|
+
_Binary.is = isArrayBufferLike;
|
|
88
|
+
_Binary.isSource = isArrayBufferSource;
|
|
89
|
+
function fromSource(source) {
|
|
90
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
91
|
+
else return source;
|
|
92
|
+
}
|
|
93
|
+
_Binary.fromSource = fromSource;
|
|
94
|
+
function toBase64(source) {
|
|
95
|
+
source = fromSource(source);
|
|
96
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
|
|
97
|
+
let binary = "";
|
|
98
|
+
const bytes = new Uint8Array(source);
|
|
99
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
100
|
+
return btoa(binary);
|
|
101
|
+
}
|
|
102
|
+
_Binary.toBase64 = toBase64;
|
|
103
|
+
function fromBase64(source) {
|
|
104
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
|
|
105
|
+
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
|
|
106
|
+
}
|
|
107
|
+
_Binary.fromBase64 = fromBase64;
|
|
108
|
+
function toHex(source) {
|
|
109
|
+
source = fromSource(source);
|
|
110
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
|
|
111
|
+
return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
112
|
+
}
|
|
113
|
+
_Binary.toHex = toHex;
|
|
114
|
+
function fromHex(source) {
|
|
115
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
|
|
116
|
+
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
|
|
117
|
+
const buffer = [];
|
|
118
|
+
for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
|
|
119
|
+
return Uint8Array.from(buffer).buffer;
|
|
120
|
+
}
|
|
121
|
+
_Binary.fromHex = fromHex;
|
|
122
|
+
})(Binary || (Binary = {}));
|
|
123
|
+
Binary.fromBase64;
|
|
124
|
+
Binary.toBase64;
|
|
125
|
+
Binary.fromHex;
|
|
126
|
+
Binary.toHex;
|
|
127
|
+
/** Deep-clone common JavaScript values while preserving prototypes and cycles. */
|
|
128
|
+
function clone(source, refs = /* @__PURE__ */ new Map()) {
|
|
129
|
+
if (!source || typeof source !== "object") return source;
|
|
130
|
+
if (is("Date", source)) return new Date(source.valueOf());
|
|
131
|
+
if (is("RegExp", source)) return new RegExp(source.source, source.flags);
|
|
132
|
+
if (isArrayBufferLike(source)) return source.slice(0);
|
|
133
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
134
|
+
const cached = refs.get(source);
|
|
135
|
+
if (cached) return cached;
|
|
136
|
+
if (Array.isArray(source)) {
|
|
137
|
+
const result = [];
|
|
138
|
+
refs.set(source, result);
|
|
139
|
+
source.forEach((value, index) => {
|
|
140
|
+
result[index] = Reflect.apply(clone, null, [value, refs]);
|
|
141
|
+
});
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
const result = Object.create(Object.getPrototypeOf(source));
|
|
145
|
+
refs.set(source, result);
|
|
146
|
+
for (const key of Reflect.ownKeys(source)) {
|
|
147
|
+
const descriptor = { ...Reflect.getOwnPropertyDescriptor(source, key) };
|
|
148
|
+
if ("value" in descriptor) descriptor.value = Reflect.apply(clone, null, [descriptor.value, refs]);
|
|
149
|
+
Reflect.defineProperty(result, key, descriptor);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Compare values recursively, treating two volatile references as equal regardless of value.
|
|
155
|
+
* Strict comparison distinguishes null/undefined, treats opaque objects by identity,
|
|
156
|
+
* compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
|
|
157
|
+
* @param a - first value.
|
|
158
|
+
* @param b - second value.
|
|
159
|
+
* @param strict - whether to require strict data equality outside volatile references.
|
|
160
|
+
* @returns whether the values compare equal.
|
|
161
|
+
*/
|
|
162
|
+
function deepEqual(a, b, strict) {
|
|
163
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
164
|
+
function compare(a, b) {
|
|
165
|
+
if (a === b) return true;
|
|
166
|
+
if (isVolatile(a) || isVolatile(b)) return isVolatile(a) && isVolatile(b);
|
|
167
|
+
if (!strict && isNullable(a) && isNullable(b)) return true;
|
|
168
|
+
if (typeof a !== typeof b || typeof a !== "object" || !a || !b) return false;
|
|
169
|
+
if (ancestors.has(a)) return false;
|
|
170
|
+
function check(test, then) {
|
|
171
|
+
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
|
|
172
|
+
}
|
|
173
|
+
ancestors.add(a);
|
|
174
|
+
try {
|
|
175
|
+
return check(Array.isArray, (a, b) => {
|
|
176
|
+
if (a.length !== b.length) return false;
|
|
177
|
+
for (let index = 0; index < a.length; index++) if (!compare(a[index], b[index])) return false;
|
|
178
|
+
return true;
|
|
179
|
+
}) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("URL"), (a, b) => a.href === b.href) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
|
|
180
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
181
|
+
const viewA = new Uint8Array(a);
|
|
182
|
+
const viewB = new Uint8Array(b);
|
|
183
|
+
for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
|
|
184
|
+
return true;
|
|
185
|
+
}) ?? ((!strict || [a, b].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
|
|
186
|
+
...a,
|
|
187
|
+
...b
|
|
188
|
+
}).every((key) => compare(a[key], b[key])));
|
|
189
|
+
} finally {
|
|
190
|
+
ancestors.delete(a);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return compare(a, b);
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region ../../../vendor/cosmokit/src/time.ts
|
|
197
|
+
let Time;
|
|
198
|
+
(function(_Time) {
|
|
199
|
+
_Time.millisecond = 1;
|
|
200
|
+
const second = _Time.second = 1e3;
|
|
201
|
+
const minute = _Time.minute = second * 60;
|
|
202
|
+
const hour = _Time.hour = minute * 60;
|
|
203
|
+
const day = _Time.day = hour * 24;
|
|
204
|
+
const week = _Time.week = day * 7;
|
|
205
|
+
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
206
|
+
function setTimezoneOffset(offset) {
|
|
207
|
+
timezoneOffset = offset;
|
|
208
|
+
}
|
|
209
|
+
_Time.setTimezoneOffset = setTimezoneOffset;
|
|
210
|
+
function getTimezoneOffset() {
|
|
211
|
+
return timezoneOffset;
|
|
212
|
+
}
|
|
213
|
+
_Time.getTimezoneOffset = getTimezoneOffset;
|
|
214
|
+
function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
|
|
215
|
+
if (typeof date === "number") date = new Date(date);
|
|
216
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
217
|
+
return Math.floor((date.valueOf() / minute - offset) / 1440);
|
|
218
|
+
}
|
|
219
|
+
_Time.getDateNumber = getDateNumber;
|
|
220
|
+
function fromDateNumber(value, offset) {
|
|
221
|
+
const date = new Date(value * day);
|
|
222
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
223
|
+
return new Date(+date + offset * minute);
|
|
224
|
+
}
|
|
225
|
+
_Time.fromDateNumber = fromDateNumber;
|
|
226
|
+
const numeric = /\d+(?:\.\d+)?/.source;
|
|
227
|
+
const timeRegExp = new RegExp(`^${[
|
|
228
|
+
"w(?:eek(?:s)?)?",
|
|
229
|
+
"d(?:ay(?:s)?)?",
|
|
230
|
+
"h(?:our(?:s)?)?",
|
|
231
|
+
"m(?:in(?:ute)?(?:s)?)?",
|
|
232
|
+
"s(?:ec(?:ond)?(?:s)?)?"
|
|
233
|
+
].map((unit) => `(${numeric}${unit})?`).join("")}$`);
|
|
234
|
+
function parseTime(source) {
|
|
235
|
+
const capture = timeRegExp.exec(source);
|
|
236
|
+
if (!capture) return 0;
|
|
237
|
+
return (parseFloat(capture[1]) * week || 0) + (parseFloat(capture[2]) * day || 0) + (parseFloat(capture[3]) * hour || 0) + (parseFloat(capture[4]) * minute || 0) + (parseFloat(capture[5]) * second || 0);
|
|
238
|
+
}
|
|
239
|
+
_Time.parseTime = parseTime;
|
|
240
|
+
function parseDate(date) {
|
|
241
|
+
const parsed = parseTime(date);
|
|
242
|
+
if (parsed) date = Date.now() + parsed;
|
|
243
|
+
else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
|
|
244
|
+
else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
|
|
245
|
+
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
246
|
+
}
|
|
247
|
+
_Time.parseDate = parseDate;
|
|
248
|
+
function format(ms) {
|
|
249
|
+
const abs = Math.abs(ms);
|
|
250
|
+
if (abs >= day - hour / 2) return Math.round(ms / day) + "d";
|
|
251
|
+
else if (abs >= hour - minute / 2) return Math.round(ms / hour) + "h";
|
|
252
|
+
else if (abs >= minute - second / 2) return Math.round(ms / minute) + "m";
|
|
253
|
+
else if (abs >= second) return Math.round(ms / second) + "s";
|
|
254
|
+
return ms + "ms";
|
|
255
|
+
}
|
|
256
|
+
_Time.format = format;
|
|
257
|
+
function toDigits(source, length = 2) {
|
|
258
|
+
return source.toString().padStart(length, "0");
|
|
259
|
+
}
|
|
260
|
+
_Time.toDigits = toDigits;
|
|
261
|
+
function template(template, time = /* @__PURE__ */ new Date()) {
|
|
262
|
+
return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
|
|
263
|
+
}
|
|
264
|
+
_Time.template = template;
|
|
265
|
+
})(Time || (Time = {}));
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region ../../../vendor/schemastery/src/index.ts
|
|
268
|
+
const kSchema = Symbol.for("schemastery");
|
|
269
|
+
const kValidationError = Symbol.for("ValidationError");
|
|
270
|
+
globalThis.__schemastery_index__ ??= 0;
|
|
271
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
272
|
+
var ValidationError = class extends TypeError {
|
|
273
|
+
options;
|
|
274
|
+
name = "ValidationError";
|
|
275
|
+
constructor(message, options) {
|
|
276
|
+
let prefix = "$";
|
|
277
|
+
for (const segment of options.path || []) if (typeof segment === "string") prefix += "." + segment;
|
|
278
|
+
else if (typeof segment === "number") prefix += "[" + segment + "]";
|
|
279
|
+
else if (typeof segment === "symbol") prefix += `[Symbol(${segment.toString()})]`;
|
|
280
|
+
if (prefix.startsWith(".")) prefix = prefix.slice(1);
|
|
281
|
+
super((prefix === "$" ? "" : `${prefix} `) + message);
|
|
282
|
+
this.options = options;
|
|
283
|
+
}
|
|
284
|
+
static is(error) {
|
|
285
|
+
return !!error?.[kValidationError];
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
Object.defineProperty(ValidationError.prototype, kValidationError, { value: true });
|
|
289
|
+
const Schema = function(options) {
|
|
290
|
+
const schema = function(data, options = {}) {
|
|
291
|
+
return Schema.resolve(data, schema, options)[0];
|
|
292
|
+
};
|
|
293
|
+
if (options.refs) {
|
|
294
|
+
const refs = mapValues(options.refs, (options) => new Schema(options));
|
|
295
|
+
const getRef = (uid) => refs[uid];
|
|
296
|
+
for (const key in refs) {
|
|
297
|
+
const options = refs[key];
|
|
298
|
+
options.sKey = getRef(options.sKey);
|
|
299
|
+
options.inner = getRef(options.inner);
|
|
300
|
+
options.list = options.list && options.list.map(getRef);
|
|
301
|
+
options.dict = options.dict && mapValues(options.dict, getRef);
|
|
302
|
+
}
|
|
303
|
+
return refs[options.uid];
|
|
304
|
+
}
|
|
305
|
+
Object.assign(schema, options);
|
|
306
|
+
if (typeof schema.callback === "string") try {
|
|
307
|
+
schema.callback = new Function("return " + schema.callback)();
|
|
308
|
+
} catch {}
|
|
309
|
+
Object.defineProperty(schema, "uid", { value: globalThis.__schemastery_index__++ });
|
|
310
|
+
Object.setPrototypeOf(schema, Schema.prototype);
|
|
311
|
+
schema.meta ||= {};
|
|
312
|
+
schema.toString = schema.toString.bind(schema);
|
|
313
|
+
return schema;
|
|
314
|
+
};
|
|
315
|
+
Schema.prototype = Object.create(Function.prototype);
|
|
316
|
+
Schema.prototype[kSchema] = true;
|
|
317
|
+
Object.defineProperty(Schema.prototype, "~standard", { get() {
|
|
318
|
+
return {
|
|
319
|
+
version: 1,
|
|
320
|
+
vendor: "schemastery",
|
|
321
|
+
validate: (value) => {
|
|
322
|
+
try {
|
|
323
|
+
return { value: Schema.resolve(value, this, {})[0] };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
if (ValidationError.is(error)) return { issues: [{
|
|
326
|
+
message: error.message,
|
|
327
|
+
path: error.options.path
|
|
328
|
+
}] };
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
} });
|
|
334
|
+
Schema.ValidationError = ValidationError;
|
|
335
|
+
Schema.prototype.toJSON = function toJSON() {
|
|
336
|
+
if (globalThis.__schemastery_refs__) {
|
|
337
|
+
globalThis.__schemastery_refs__[this.uid] ??= JSON.parse(JSON.stringify({ ...this }));
|
|
338
|
+
return this.uid;
|
|
339
|
+
}
|
|
340
|
+
globalThis.__schemastery_refs__ = { [this.uid]: { ...this } };
|
|
341
|
+
globalThis.__schemastery_refs__[this.uid] = JSON.parse(JSON.stringify({ ...this }));
|
|
342
|
+
const result = {
|
|
343
|
+
uid: this.uid,
|
|
344
|
+
refs: globalThis.__schemastery_refs__
|
|
345
|
+
};
|
|
346
|
+
globalThis.__schemastery_refs__ = void 0;
|
|
347
|
+
return result;
|
|
348
|
+
};
|
|
349
|
+
Schema.prototype.set = function set(key, value) {
|
|
350
|
+
this.dict[key] = value;
|
|
351
|
+
return this;
|
|
352
|
+
};
|
|
353
|
+
Schema.prototype.push = function push(value) {
|
|
354
|
+
this.list.push(value);
|
|
355
|
+
return this;
|
|
356
|
+
};
|
|
357
|
+
function mergeDesc(original, messages) {
|
|
358
|
+
const result = typeof original === "string" ? { "": original } : { ...original };
|
|
359
|
+
for (const locale in messages) {
|
|
360
|
+
const value = messages[locale];
|
|
361
|
+
if (value?.$description || value?.$desc) result[locale] = value.$description || value.$desc;
|
|
362
|
+
else if (typeof value === "string") result[locale] = value;
|
|
363
|
+
}
|
|
364
|
+
return result;
|
|
365
|
+
}
|
|
366
|
+
function getInner(value) {
|
|
367
|
+
return value?.$value ?? value?.$inner;
|
|
368
|
+
}
|
|
369
|
+
function extractKeys(data) {
|
|
370
|
+
return filterKeys(data ?? {}, (key) => !key.startsWith("$"));
|
|
371
|
+
}
|
|
372
|
+
Schema.prototype.i18n = function i18n(messages) {
|
|
373
|
+
const schema = Schema(this);
|
|
374
|
+
const desc = mergeDesc(schema.meta.description, messages);
|
|
375
|
+
if (Object.keys(desc).length) schema.meta.description = desc;
|
|
376
|
+
if (schema.dict) schema.dict = mapValues(schema.dict, (inner, key) => {
|
|
377
|
+
return inner.i18n(mapValues(messages, (data) => getInner(data)?.[key] ?? data?.[key]));
|
|
378
|
+
});
|
|
379
|
+
if (schema.list) schema.list = schema.list.map((inner, index) => {
|
|
380
|
+
return inner.i18n(mapValues(messages, (data = {}) => {
|
|
381
|
+
if (Array.isArray(getInner(data))) return getInner(data)[index];
|
|
382
|
+
if (Array.isArray(data)) return data[index];
|
|
383
|
+
return extractKeys(data);
|
|
384
|
+
}));
|
|
385
|
+
});
|
|
386
|
+
if (schema.inner) schema.inner = schema.inner.i18n(mapValues(messages, (data) => {
|
|
387
|
+
if (getInner(data)) return getInner(data);
|
|
388
|
+
return extractKeys(data);
|
|
389
|
+
}));
|
|
390
|
+
if (schema.sKey) schema.sKey = schema.sKey.i18n(mapValues(messages, (data) => data?.$key));
|
|
391
|
+
return schema;
|
|
392
|
+
};
|
|
393
|
+
Schema.prototype.extra = function extra(key, value) {
|
|
394
|
+
const schema = Schema(this);
|
|
395
|
+
schema.meta = {
|
|
396
|
+
...schema.meta,
|
|
397
|
+
[key]: value
|
|
398
|
+
};
|
|
399
|
+
return schema;
|
|
400
|
+
};
|
|
401
|
+
for (const key of [
|
|
402
|
+
"required",
|
|
403
|
+
"disabled",
|
|
404
|
+
"collapse",
|
|
405
|
+
"hidden",
|
|
406
|
+
"loose"
|
|
407
|
+
]) Object.assign(Schema.prototype, { [key](value = true) {
|
|
408
|
+
const schema = Schema(this);
|
|
409
|
+
schema.meta = {
|
|
410
|
+
...schema.meta,
|
|
411
|
+
[key]: value
|
|
412
|
+
};
|
|
413
|
+
return schema;
|
|
414
|
+
} });
|
|
415
|
+
Schema.prototype.deprecated = function deprecated() {
|
|
416
|
+
const schema = Schema(this);
|
|
417
|
+
schema.meta.badges ||= [];
|
|
418
|
+
schema.meta.badges.push({
|
|
419
|
+
text: "deprecated",
|
|
420
|
+
type: "danger"
|
|
421
|
+
});
|
|
422
|
+
return schema;
|
|
423
|
+
};
|
|
424
|
+
Schema.prototype.experimental = function experimental() {
|
|
425
|
+
const schema = Schema(this);
|
|
426
|
+
schema.meta.badges ||= [];
|
|
427
|
+
schema.meta.badges.push({
|
|
428
|
+
text: "experimental",
|
|
429
|
+
type: "warning"
|
|
430
|
+
});
|
|
431
|
+
return schema;
|
|
432
|
+
};
|
|
433
|
+
Schema.prototype.pattern = function pattern(regexp) {
|
|
434
|
+
const schema = Schema(this);
|
|
435
|
+
const pattern = pick(regexp, ["source", "flags"]);
|
|
436
|
+
schema.meta = {
|
|
437
|
+
...schema.meta,
|
|
438
|
+
pattern
|
|
439
|
+
};
|
|
440
|
+
return schema;
|
|
441
|
+
};
|
|
442
|
+
Schema.prototype.simplify = function simplify(value) {
|
|
443
|
+
if (isVolatile(value)) value = value.get();
|
|
444
|
+
if (deepEqual(value, this.meta.default, this.type === "dict")) return null;
|
|
445
|
+
if (isNullable(value)) return value;
|
|
446
|
+
if (this.type === "object" || this.type === "dict") {
|
|
447
|
+
const result = {};
|
|
448
|
+
for (const key in value) {
|
|
449
|
+
const item = (this.type === "object" ? this.dict[key] : this.inner)?.simplify(value[key]);
|
|
450
|
+
if (this.type === "dict" || !isNullable(item)) result[key] = item;
|
|
451
|
+
}
|
|
452
|
+
if (deepEqual(result, this.meta.default, this.type === "dict")) return null;
|
|
453
|
+
return result;
|
|
454
|
+
} else if (this.type === "array" || this.type === "tuple") {
|
|
455
|
+
const result = [];
|
|
456
|
+
value.forEach((value, index) => {
|
|
457
|
+
const schema = this.type === "array" ? this.inner : this.list[index];
|
|
458
|
+
const item = schema ? schema.simplify(value) : value;
|
|
459
|
+
result.push(item);
|
|
460
|
+
});
|
|
461
|
+
return result;
|
|
462
|
+
} else if (this.type === "intersect") {
|
|
463
|
+
const result = {};
|
|
464
|
+
for (const item of this.list) Object.assign(result, item.simplify(value));
|
|
465
|
+
return result;
|
|
466
|
+
} else if (this.type === "union") for (const schema of this.list) try {
|
|
467
|
+
Schema.resolve(value, schema, {});
|
|
468
|
+
return schema.simplify(value);
|
|
469
|
+
} catch {}
|
|
470
|
+
return value;
|
|
471
|
+
};
|
|
472
|
+
Schema.prototype.toString = function toString(inline) {
|
|
473
|
+
return formatters[this.type]?.(this, inline) ?? `Schema<${this.type}>`;
|
|
474
|
+
};
|
|
475
|
+
Schema.prototype.role = function role(role, extra) {
|
|
476
|
+
const schema = Schema(this);
|
|
477
|
+
schema.meta = {
|
|
478
|
+
...schema.meta,
|
|
479
|
+
role,
|
|
480
|
+
extra
|
|
481
|
+
};
|
|
482
|
+
return schema;
|
|
483
|
+
};
|
|
484
|
+
for (const key of [
|
|
485
|
+
"default",
|
|
486
|
+
"link",
|
|
487
|
+
"comment",
|
|
488
|
+
"description",
|
|
489
|
+
"max",
|
|
490
|
+
"min",
|
|
491
|
+
"step"
|
|
492
|
+
]) Object.assign(Schema.prototype, { [key](value) {
|
|
493
|
+
const schema = Schema(this);
|
|
494
|
+
schema.meta = {
|
|
495
|
+
...schema.meta,
|
|
496
|
+
[key]: value
|
|
497
|
+
};
|
|
498
|
+
return schema;
|
|
499
|
+
} });
|
|
500
|
+
Schema.prototype.volatile = function volatile() {
|
|
501
|
+
if (this.meta.volatile) throw new TypeError("volatile schema is already wrapped");
|
|
502
|
+
return this.extra("volatile", true);
|
|
503
|
+
};
|
|
504
|
+
const resolvers = {};
|
|
505
|
+
const checkedVolatile = Symbol("checked-volatile-schema");
|
|
506
|
+
function validateVolatileSchema(schema, path = [], blocked = false, seen = /* @__PURE__ */ new Map()) {
|
|
507
|
+
const states = seen.get(schema) ?? /* @__PURE__ */ new Set();
|
|
508
|
+
if (states.has(blocked)) return;
|
|
509
|
+
states.add(blocked);
|
|
510
|
+
seen.set(schema, states);
|
|
511
|
+
if (schema.meta?.volatile && blocked) throw new ValidationError("volatile fields require a fixed object path without an enclosing volatile field", { path });
|
|
512
|
+
const nested = blocked || !!schema.meta?.volatile;
|
|
513
|
+
if (schema.dict) for (const [key, child] of Object.entries(schema.dict)) validateVolatileSchema(child, [...path, key], nested, seen);
|
|
514
|
+
if (schema.sKey) validateVolatileSchema(schema.sKey, [...path, "<key>"], true, seen);
|
|
515
|
+
if (schema.inner && (schema.type !== "lazy" || schema.inner[kSchema])) validateVolatileSchema(schema.inner, [...path, "*"], true, seen);
|
|
516
|
+
if (schema.list) for (let index = 0; index < schema.list.length; index++) validateVolatileSchema(schema.list[index], [...path, String(index)], true, seen);
|
|
517
|
+
}
|
|
518
|
+
Schema.extend = function extend(type, resolve) {
|
|
519
|
+
resolvers[type] = resolve;
|
|
520
|
+
};
|
|
521
|
+
Schema.resolve = function resolve(data, schema, options = {}, strict = false) {
|
|
522
|
+
if (!schema) return [data];
|
|
523
|
+
if (!options[checkedVolatile]) {
|
|
524
|
+
validateVolatileSchema(schema, options.path);
|
|
525
|
+
options = {
|
|
526
|
+
...options,
|
|
527
|
+
[checkedVolatile]: true
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if (schema.meta?.volatile) {
|
|
531
|
+
const inner = Schema(schema);
|
|
532
|
+
inner.meta = {
|
|
533
|
+
...schema.meta,
|
|
534
|
+
volatile: false
|
|
535
|
+
};
|
|
536
|
+
const [value, adapted] = Schema.resolve(data, inner, options, strict);
|
|
537
|
+
try {
|
|
538
|
+
return [createVolatile(value), adapted];
|
|
539
|
+
} catch (error) {
|
|
540
|
+
throw new ValidationError(error instanceof Error ? error.message : String(error), options);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (options.ignore?.(data, schema)) return [data];
|
|
544
|
+
if (isNullable(data) && schema.type !== "lazy") {
|
|
545
|
+
if (schema.meta.required) throw new ValidationError(`missing required value`, options);
|
|
546
|
+
let current = schema;
|
|
547
|
+
let fallback = schema.meta.default;
|
|
548
|
+
while (current?.type === "intersect" && isNullable(fallback)) {
|
|
549
|
+
current = current.list[0];
|
|
550
|
+
fallback = current?.meta.default;
|
|
551
|
+
}
|
|
552
|
+
if (isNullable(fallback)) return [data];
|
|
553
|
+
data = clone(fallback);
|
|
554
|
+
}
|
|
555
|
+
const callback = resolvers[schema.type];
|
|
556
|
+
if (!callback) throw new ValidationError(`unsupported type "${schema.type}"`, options);
|
|
557
|
+
try {
|
|
558
|
+
return callback(data, schema, options, strict);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (!schema.meta.loose) throw error;
|
|
561
|
+
return [schema.meta.default];
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
Schema.from = function from(source) {
|
|
565
|
+
if (isNullable(source)) return Schema.any();
|
|
566
|
+
else if ([
|
|
567
|
+
"string",
|
|
568
|
+
"number",
|
|
569
|
+
"boolean"
|
|
570
|
+
].includes(typeof source)) return Schema.const(source).required();
|
|
571
|
+
else if (source[kSchema]) return source;
|
|
572
|
+
else if (typeof source === "function") switch (source) {
|
|
573
|
+
case String: return Schema.string().required();
|
|
574
|
+
case Number: return Schema.number().required();
|
|
575
|
+
case Boolean: return Schema.boolean().required();
|
|
576
|
+
case Function: return Schema.function().required();
|
|
577
|
+
default: return Schema.is(source).required();
|
|
578
|
+
}
|
|
579
|
+
else throw new TypeError(`cannot infer schema from ${source}`);
|
|
580
|
+
};
|
|
581
|
+
Schema.lazy = function lazy(builder) {
|
|
582
|
+
const toJSON = () => {
|
|
583
|
+
if (!schema.inner[kSchema]) {
|
|
584
|
+
schema.inner = schema.builder();
|
|
585
|
+
schema.inner.meta = {
|
|
586
|
+
...schema.meta,
|
|
587
|
+
...schema.inner.meta
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
return schema.inner.toJSON();
|
|
591
|
+
};
|
|
592
|
+
const schema = new Schema({
|
|
593
|
+
type: "lazy",
|
|
594
|
+
builder,
|
|
595
|
+
inner: { toJSON }
|
|
596
|
+
});
|
|
597
|
+
return schema;
|
|
598
|
+
};
|
|
599
|
+
Schema.natural = function natural() {
|
|
600
|
+
return Schema.number().step(1).min(0);
|
|
601
|
+
};
|
|
602
|
+
Schema.percent = function percent() {
|
|
603
|
+
return Schema.number().step(.01).min(0).max(1).role("slider");
|
|
604
|
+
};
|
|
605
|
+
Schema.date = function date() {
|
|
606
|
+
return Schema.union([Schema.is(Date), Schema.transform(Schema.string().role("datetime"), (value, options) => {
|
|
607
|
+
const date = new Date(value);
|
|
608
|
+
if (isNaN(+date)) throw new ValidationError(`invalid date "${value}"`, options);
|
|
609
|
+
return date;
|
|
610
|
+
}, true)]);
|
|
611
|
+
};
|
|
612
|
+
Schema.regExp = function regExp(flag = "") {
|
|
613
|
+
return Schema.union([Schema.is(RegExp), Schema.transform(Schema.string().role("regexp", { flag }), (value, options) => {
|
|
614
|
+
try {
|
|
615
|
+
return new RegExp(value, flag);
|
|
616
|
+
} catch (e) {
|
|
617
|
+
throw new ValidationError(e.message, options);
|
|
618
|
+
}
|
|
619
|
+
}, true)]);
|
|
620
|
+
};
|
|
621
|
+
Schema.arrayBuffer = function arrayBuffer(encoding) {
|
|
622
|
+
return Schema.union([
|
|
623
|
+
Schema.is(ArrayBuffer),
|
|
624
|
+
Schema.is(SharedArrayBuffer),
|
|
625
|
+
Schema.transform(Schema.any(), (value, options) => {
|
|
626
|
+
if (Binary.isSource(value)) return Binary.fromSource(value);
|
|
627
|
+
throw new ValidationError(`expected ArrayBufferSource but got ${value}`, options);
|
|
628
|
+
}, true),
|
|
629
|
+
...encoding ? [Schema.transform(Schema.string(), (value, options) => {
|
|
630
|
+
try {
|
|
631
|
+
return encoding === "base64" ? Binary.fromBase64(value) : Binary.fromHex(value);
|
|
632
|
+
} catch (e) {
|
|
633
|
+
throw new ValidationError(e.message, options);
|
|
634
|
+
}
|
|
635
|
+
}, true)] : []
|
|
636
|
+
]);
|
|
637
|
+
};
|
|
638
|
+
Schema.extend("lazy", (data, schema, options, strict) => {
|
|
639
|
+
if (!schema.inner[kSchema]) {
|
|
640
|
+
schema.inner = schema.builder();
|
|
641
|
+
schema.inner.meta = {
|
|
642
|
+
...schema.meta,
|
|
643
|
+
...schema.inner.meta
|
|
644
|
+
};
|
|
645
|
+
validateVolatileSchema(schema.inner, options.path, true);
|
|
646
|
+
}
|
|
647
|
+
return Schema.resolve(data, schema.inner, options, strict);
|
|
648
|
+
});
|
|
649
|
+
Schema.extend("any", (data) => {
|
|
650
|
+
return [data];
|
|
651
|
+
});
|
|
652
|
+
Schema.extend("never", (data, _, options) => {
|
|
653
|
+
throw new ValidationError(`expected nullable but got ${data}`, options);
|
|
654
|
+
});
|
|
655
|
+
Schema.extend("const", (data, { value }, options) => {
|
|
656
|
+
if (deepEqual(data, value)) return [value];
|
|
657
|
+
throw new ValidationError(`expected ${value} but got ${data}`, options);
|
|
658
|
+
});
|
|
659
|
+
function checkWithinRange(data, meta, description, options, skipMin = false) {
|
|
660
|
+
const { max = Infinity, min = -Infinity } = meta;
|
|
661
|
+
if (data > max) throw new ValidationError(`expected ${description} <= ${max} but got ${data}`, options);
|
|
662
|
+
if (data < min && !skipMin) throw new ValidationError(`expected ${description} >= ${min} but got ${data}`, options);
|
|
663
|
+
}
|
|
664
|
+
Schema.extend("string", (data, { meta }, options) => {
|
|
665
|
+
if (typeof data !== "string") throw new ValidationError(`expected string but got ${data}`, options);
|
|
666
|
+
if (meta.pattern) {
|
|
667
|
+
const regexp = new RegExp(meta.pattern.source, meta.pattern.flags);
|
|
668
|
+
if (!regexp.test(data)) throw new ValidationError(`expect string to match regexp ${regexp}`, options);
|
|
669
|
+
}
|
|
670
|
+
checkWithinRange(data.length, meta, "string length", options);
|
|
671
|
+
return [data];
|
|
672
|
+
});
|
|
673
|
+
function decimalShift(data, digits) {
|
|
674
|
+
const str = data.toString();
|
|
675
|
+
if (str.includes("e")) return data * Math.pow(10, digits);
|
|
676
|
+
const index = str.indexOf(".");
|
|
677
|
+
if (index === -1) return data * Math.pow(10, digits);
|
|
678
|
+
const frac = str.slice(index + 1);
|
|
679
|
+
const integer = str.slice(0, index);
|
|
680
|
+
if (frac.length <= digits) return +(integer + frac.padEnd(digits, "0"));
|
|
681
|
+
return +(integer + frac.slice(0, digits) + "." + frac.slice(digits));
|
|
682
|
+
}
|
|
683
|
+
function isMultipleOf(data, min, step) {
|
|
684
|
+
step = Math.abs(step);
|
|
685
|
+
if (!/^\d+\.\d+$/.test(step.toString())) return (data - min) % step === 0;
|
|
686
|
+
const index = step.toString().indexOf(".");
|
|
687
|
+
const digits = step.toString().slice(index + 1).length;
|
|
688
|
+
return Math.abs(decimalShift(data, digits) - decimalShift(min, digits)) % decimalShift(step, digits) === 0;
|
|
689
|
+
}
|
|
690
|
+
Schema.extend("number", (data, { meta }, options) => {
|
|
691
|
+
if (typeof data !== "number") throw new ValidationError(`expected number but got ${data}`, options);
|
|
692
|
+
checkWithinRange(data, meta, "number", options);
|
|
693
|
+
const { step } = meta;
|
|
694
|
+
if (step && !isMultipleOf(data, meta.min ?? 0, step)) throw new ValidationError(`expected number multiple of ${step} but got ${data}`, options);
|
|
695
|
+
return [data];
|
|
696
|
+
});
|
|
697
|
+
Schema.extend("boolean", (data, _, options) => {
|
|
698
|
+
if (typeof data === "boolean") return [data];
|
|
699
|
+
throw new ValidationError(`expected boolean but got ${data}`, options);
|
|
700
|
+
});
|
|
701
|
+
Schema.extend("bitset", (data, { bits, meta }, options) => {
|
|
702
|
+
let value = 0, keys = [];
|
|
703
|
+
if (typeof data === "number") {
|
|
704
|
+
value = data;
|
|
705
|
+
for (const key in bits) if (data & bits[key]) keys.push(key);
|
|
706
|
+
} else if (Array.isArray(data)) {
|
|
707
|
+
keys = data;
|
|
708
|
+
for (const key of keys) {
|
|
709
|
+
if (typeof key !== "string") throw new ValidationError(`expected string but got ${key}`, options);
|
|
710
|
+
if (key in bits) value |= bits[key];
|
|
711
|
+
}
|
|
712
|
+
} else throw new ValidationError(`expected number or array but got ${data}`, options);
|
|
713
|
+
if (value === meta.default) return [value];
|
|
714
|
+
return [value, keys];
|
|
715
|
+
});
|
|
716
|
+
Schema.extend("function", (data, _, options) => {
|
|
717
|
+
if (typeof data === "function") return [data];
|
|
718
|
+
throw new ValidationError(`expected function but got ${data}`, options);
|
|
719
|
+
});
|
|
720
|
+
Schema.extend("is", (data, { constructor }, options) => {
|
|
721
|
+
if (typeof constructor === "function") {
|
|
722
|
+
if (data instanceof constructor) return [data];
|
|
723
|
+
throw new ValidationError(`expected ${constructor.name} but got ${data}`, options);
|
|
724
|
+
} else {
|
|
725
|
+
if (isNullable(data)) throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
726
|
+
let prototype = Object.getPrototypeOf(data);
|
|
727
|
+
while (prototype) {
|
|
728
|
+
if (prototype.constructor?.name === constructor) return [data];
|
|
729
|
+
prototype = Object.getPrototypeOf(prototype);
|
|
730
|
+
}
|
|
731
|
+
throw new ValidationError(`expected ${constructor} but got ${data}`, options);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
function property(data, key, schema, options) {
|
|
735
|
+
try {
|
|
736
|
+
const [value, adapted] = Schema.resolve(data[key], schema, {
|
|
737
|
+
...options,
|
|
738
|
+
path: [...options.path || [], key]
|
|
739
|
+
});
|
|
740
|
+
if (adapted !== void 0) data[key] = adapted;
|
|
741
|
+
return value;
|
|
742
|
+
} catch (e) {
|
|
743
|
+
if (!options?.autofix) throw e;
|
|
744
|
+
delete data[key];
|
|
745
|
+
return schema.meta.volatile ? createVolatile(schema.meta.default) : schema.meta.default;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
Schema.extend("array", (data, { inner, meta }, options) => {
|
|
749
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
750
|
+
checkWithinRange(data.length, meta, "array length", options, !isNullable(inner.meta.default));
|
|
751
|
+
return [data.map((_, index) => property(data, index, inner, options))];
|
|
752
|
+
});
|
|
753
|
+
Schema.extend("dict", (data, { inner, sKey }, options, strict) => {
|
|
754
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
755
|
+
const result = {};
|
|
756
|
+
for (const key in data) {
|
|
757
|
+
let rKey;
|
|
758
|
+
try {
|
|
759
|
+
rKey = Schema.resolve(key, sKey, options)[0];
|
|
760
|
+
} catch (error) {
|
|
761
|
+
if (strict) continue;
|
|
762
|
+
throw error;
|
|
763
|
+
}
|
|
764
|
+
result[rKey] = property(data, key, inner, options);
|
|
765
|
+
data[rKey] = data[key];
|
|
766
|
+
if (key !== rKey) delete data[key];
|
|
767
|
+
}
|
|
768
|
+
return [result];
|
|
769
|
+
});
|
|
770
|
+
Schema.extend("tuple", (data, { list }, options, strict) => {
|
|
771
|
+
if (!Array.isArray(data)) throw new ValidationError(`expected array but got ${data}`, options);
|
|
772
|
+
const result = list.map((inner, index) => property(data, index, inner, options));
|
|
773
|
+
if (strict) return [result];
|
|
774
|
+
result.push(...data.slice(list.length));
|
|
775
|
+
return [result];
|
|
776
|
+
});
|
|
777
|
+
function merge(result, data) {
|
|
778
|
+
for (const key in data) {
|
|
779
|
+
if (key in result) continue;
|
|
780
|
+
result[key] = data[key];
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
Schema.extend("object", (data, { dict }, options, strict) => {
|
|
784
|
+
if (!isPlainObject(data)) throw new ValidationError(`expected object but got ${data}`, options);
|
|
785
|
+
const result = {};
|
|
786
|
+
for (const key in dict) {
|
|
787
|
+
const value = property(data, key, dict[key], options);
|
|
788
|
+
if (!isNullable(value) || key in data) result[key] = value;
|
|
789
|
+
}
|
|
790
|
+
if (!strict) merge(result, data);
|
|
791
|
+
return [result];
|
|
792
|
+
});
|
|
793
|
+
Schema.extend("union", (data, { list, toString }, options, strict) => {
|
|
794
|
+
const messages = [];
|
|
795
|
+
for (const inner of list) try {
|
|
796
|
+
return Schema.resolve(data, inner, options, strict);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
messages.push(error);
|
|
799
|
+
}
|
|
800
|
+
throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
801
|
+
});
|
|
802
|
+
Schema.extend("intersect", (data, { list, toString }, options, strict) => {
|
|
803
|
+
if (!list.length) return [data];
|
|
804
|
+
let result;
|
|
805
|
+
for (const inner of list) {
|
|
806
|
+
const value = Schema.resolve(data, inner, options, true)[0];
|
|
807
|
+
if (isNullable(value)) continue;
|
|
808
|
+
if (isNullable(result)) result = value;
|
|
809
|
+
else if (typeof result !== typeof value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
810
|
+
else if (typeof value === "object") merge(result ??= {}, value);
|
|
811
|
+
else if (result !== value) throw new ValidationError(`expected ${toString()} but got ${JSON.stringify(data)}`, options);
|
|
812
|
+
}
|
|
813
|
+
if (!strict && isPlainObject(data)) merge(result, data);
|
|
814
|
+
return [result];
|
|
815
|
+
});
|
|
816
|
+
Schema.extend("transform", (data, { inner, callback, preserve }, options) => {
|
|
817
|
+
const [result, adapted = data] = Schema.resolve(data, inner, options, true);
|
|
818
|
+
if (preserve) return [callback(result)];
|
|
819
|
+
else return [callback(result), callback(adapted)];
|
|
820
|
+
});
|
|
821
|
+
const formatters = {};
|
|
822
|
+
function defineMethod(name, keys, format) {
|
|
823
|
+
formatters[name] = format;
|
|
824
|
+
Object.assign(Schema, { [name](...args) {
|
|
825
|
+
const schema = new Schema({ type: name });
|
|
826
|
+
keys.forEach((key, index) => {
|
|
827
|
+
switch (key) {
|
|
828
|
+
case "sKey":
|
|
829
|
+
schema.sKey = args[index] ?? Schema.string();
|
|
830
|
+
break;
|
|
831
|
+
case "inner":
|
|
832
|
+
schema.inner = Schema.from(args[index]);
|
|
833
|
+
break;
|
|
834
|
+
case "list":
|
|
835
|
+
schema.list = args[index].map(Schema.from);
|
|
836
|
+
break;
|
|
837
|
+
case "dict":
|
|
838
|
+
schema.dict = mapValues(args[index], Schema.from);
|
|
839
|
+
break;
|
|
840
|
+
case "bits":
|
|
841
|
+
schema.bits = {};
|
|
842
|
+
for (const key in args[index]) {
|
|
843
|
+
if (typeof args[index][key] !== "number") continue;
|
|
844
|
+
schema.bits[key] = args[index][key];
|
|
845
|
+
}
|
|
846
|
+
break;
|
|
847
|
+
case "callback": {
|
|
848
|
+
const callback = schema.callback = args[index];
|
|
849
|
+
callback["toJSON"] ||= () => callback.toString();
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
case "constructor": {
|
|
853
|
+
const constructor = schema.constructor = args[index];
|
|
854
|
+
if (typeof constructor === "function") constructor["toJSON"] ||= () => constructor["name"];
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
default: schema[key] = args[index];
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
if (name === "object" || name === "dict") schema.meta.default = {};
|
|
861
|
+
else if (name === "array" || name === "tuple") schema.meta.default = [];
|
|
862
|
+
else if (name === "bitset") schema.meta.default = 0;
|
|
863
|
+
return schema;
|
|
864
|
+
} });
|
|
865
|
+
}
|
|
866
|
+
defineMethod("is", ["constructor"], ({ constructor }) => {
|
|
867
|
+
if (typeof constructor === "function") return constructor.name;
|
|
868
|
+
else return constructor;
|
|
869
|
+
});
|
|
870
|
+
defineMethod("any", [], () => "any");
|
|
871
|
+
defineMethod("never", [], () => "never");
|
|
872
|
+
defineMethod("const", ["value"], ({ value }) => typeof value === "string" ? JSON.stringify(value) : value);
|
|
873
|
+
defineMethod("string", [], () => "string");
|
|
874
|
+
defineMethod("number", [], () => "number");
|
|
875
|
+
defineMethod("boolean", [], () => "boolean");
|
|
876
|
+
defineMethod("bitset", ["bits"], () => "bitset");
|
|
877
|
+
defineMethod("function", [], () => "function");
|
|
878
|
+
defineMethod("array", ["inner"], ({ inner }) => `${inner.toString(true)}[]`);
|
|
879
|
+
defineMethod("dict", ["inner", "sKey"], ({ inner, sKey }) => `{ [key: ${sKey.toString()}]: ${inner.toString()} }`);
|
|
880
|
+
defineMethod("tuple", ["list"], ({ list }) => `[${list.map((inner) => inner.toString()).join(", ")}]`);
|
|
881
|
+
defineMethod("object", ["dict"], ({ dict }) => {
|
|
882
|
+
if (Object.keys(dict).length === 0) return "{}";
|
|
883
|
+
return `{ ${Object.entries(dict).map(([key, inner]) => {
|
|
884
|
+
return `${key}${inner.meta.required ? "" : "?"}: ${inner.toString()}`;
|
|
885
|
+
}).join(", ")} }`;
|
|
886
|
+
});
|
|
887
|
+
defineMethod("union", ["list"], ({ list }, inline) => {
|
|
888
|
+
const result = list.map(({ toString: format }) => format()).join(" | ");
|
|
889
|
+
return inline ? `(${result})` : result;
|
|
890
|
+
});
|
|
891
|
+
defineMethod("intersect", ["list"], ({ list }) => {
|
|
892
|
+
return `${list.map((inner) => inner.toString(true)).join(" & ")}`;
|
|
893
|
+
});
|
|
894
|
+
defineMethod("transform", [
|
|
895
|
+
"inner",
|
|
896
|
+
"callback",
|
|
897
|
+
"preserve"
|
|
898
|
+
], ({ inner }, isInner) => inner.toString(isInner));
|
|
899
|
+
//#endregion
|
|
900
|
+
//#region lib/types/client/schema.js
|
|
901
|
+
/** Synchronous schema introspection and immutable settings-draft edits. */
|
|
902
|
+
function cloneContainer(container, key) {
|
|
903
|
+
if (Array.isArray(container)) return [...container];
|
|
904
|
+
if (typeof container === "object" && container !== null) return { ...container };
|
|
905
|
+
return /^\d+$/.test(key) ? [] : {};
|
|
906
|
+
}
|
|
907
|
+
function cloneSpine(root, path) {
|
|
908
|
+
const result = { ...root };
|
|
909
|
+
let target = result;
|
|
910
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
911
|
+
const key = path[index];
|
|
912
|
+
const child = cloneContainer(Array.isArray(target) ? target[Number(key)] : target[key], path[index + 1]);
|
|
913
|
+
if (Array.isArray(target)) target[Number(key)] = child;
|
|
914
|
+
else target[key] = child;
|
|
915
|
+
target = child;
|
|
916
|
+
}
|
|
917
|
+
return {
|
|
918
|
+
result,
|
|
919
|
+
parent: target,
|
|
920
|
+
leaf: path[path.length - 1]
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Settings-owned synchronous schema service. Dynamic client plugins receive
|
|
925
|
+
* this Cordis entity instead of importing executable helpers from one another.
|
|
926
|
+
*/
|
|
927
|
+
var SettingsSchemaService = class extends _qilin_agent_kylin.Service {
|
|
928
|
+
/** @param ctx - providing ui-settings context. */
|
|
929
|
+
constructor(ctx) {
|
|
930
|
+
super(ctx, "settingsSchema");
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Rehydrate one serialized `schema.toJSON()` envelope.
|
|
934
|
+
* @param serialized - serialized Schemastery node.
|
|
935
|
+
* @returns live schema node.
|
|
936
|
+
*/
|
|
937
|
+
rehydrate(serialized) {
|
|
938
|
+
return new Schema(serialized);
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Validate a settings draft.
|
|
942
|
+
* @param schema - live schema node.
|
|
943
|
+
* @param draft - candidate settings value.
|
|
944
|
+
* @returns validation failure text, or `undefined` when valid.
|
|
945
|
+
*/
|
|
946
|
+
validate(schema, draft) {
|
|
947
|
+
try {
|
|
948
|
+
schema(draft);
|
|
949
|
+
return;
|
|
950
|
+
} catch (error) {
|
|
951
|
+
return error instanceof Error ? error.message : String(error);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* Resolve an object, dict, or array schema node at a settings path.
|
|
956
|
+
* @param root - schema node to traverse.
|
|
957
|
+
* @param path - object keys or array indexes.
|
|
958
|
+
* @returns the resolved node, or `undefined` when the path is absent.
|
|
959
|
+
*/
|
|
960
|
+
nodeAtPath(root, path) {
|
|
961
|
+
let node = root;
|
|
962
|
+
for (const key of path) {
|
|
963
|
+
if (node === void 0) return void 0;
|
|
964
|
+
if (node.type === "object") node = node.dict?.[key];
|
|
965
|
+
else if (node.type === "dict" || node.type === "array") node = node.inner;
|
|
966
|
+
else return void 0;
|
|
967
|
+
}
|
|
968
|
+
return node;
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Read a nested value by a string-key or array-index path.
|
|
972
|
+
* @param value - value to traverse.
|
|
973
|
+
* @param path - object keys or array indexes.
|
|
974
|
+
* @returns the resolved value, or `undefined` when the path is absent.
|
|
975
|
+
*/
|
|
976
|
+
getPath(value, path) {
|
|
977
|
+
let current = value;
|
|
978
|
+
for (const key of path) {
|
|
979
|
+
if (Array.isArray(current)) {
|
|
980
|
+
current = current[Number(key)];
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
983
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
984
|
+
current = current[key];
|
|
985
|
+
}
|
|
986
|
+
return current;
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Report whether the final path key exists independently of its value.
|
|
990
|
+
* @param value - value to traverse.
|
|
991
|
+
* @param path - object keys or array indexes.
|
|
992
|
+
* @returns whether the path exists.
|
|
993
|
+
*/
|
|
994
|
+
hasPath(value, path) {
|
|
995
|
+
if (path.length === 0) return value !== void 0;
|
|
996
|
+
const parent = this.getPath(value, path.slice(0, -1));
|
|
997
|
+
const key = path[path.length - 1];
|
|
998
|
+
if (Array.isArray(parent)) return Number(key) < parent.length;
|
|
999
|
+
if (typeof parent !== "object" || parent === null) return false;
|
|
1000
|
+
return key in parent;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Immutably set a nested value, materializing missing containers.
|
|
1004
|
+
* @param root - settings object to copy.
|
|
1005
|
+
* @param path - non-empty object-key or array-index path.
|
|
1006
|
+
* @param value - replacement value.
|
|
1007
|
+
* @returns copied root containing the replacement.
|
|
1008
|
+
* @throws when `path` is empty.
|
|
1009
|
+
*/
|
|
1010
|
+
setPath(root, path, value) {
|
|
1011
|
+
if (path.length === 0) throw new Error("ui-settings: setPath needs a non-empty path");
|
|
1012
|
+
const { result, parent, leaf } = cloneSpine(root, path);
|
|
1013
|
+
if (Array.isArray(parent)) parent[Number(leaf)] = value;
|
|
1014
|
+
else parent[leaf] = value;
|
|
1015
|
+
return result;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Immutably remove a nested key, preserving an unchanged missing root.
|
|
1019
|
+
* @param root - settings object to copy.
|
|
1020
|
+
* @param path - non-empty object-key or array-index path.
|
|
1021
|
+
* @returns copied root without the key, or `root` when the path is absent.
|
|
1022
|
+
* @throws when `path` is empty.
|
|
1023
|
+
*/
|
|
1024
|
+
deletePath(root, path) {
|
|
1025
|
+
if (path.length === 0) throw new Error("ui-settings: deletePath needs a non-empty path");
|
|
1026
|
+
if (!this.hasPath(root, path)) return root;
|
|
1027
|
+
const { result, parent, leaf } = cloneSpine(root, path);
|
|
1028
|
+
if (Array.isArray(parent)) parent.splice(Number(leaf), 1);
|
|
1029
|
+
else Reflect.deleteProperty(parent, leaf);
|
|
1030
|
+
return result;
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region lib/types/client/developer-tools.js
|
|
1035
|
+
/** One accepted preference drives every developer-tool consumer. */
|
|
1036
|
+
/** Shared preference; Host-backed features stay disabled until an accepted value arrives. */
|
|
1037
|
+
var DeveloperToolsPreference = class {
|
|
1038
|
+
scope;
|
|
1039
|
+
/** Accepted enablement, observable through renderer-bound hooks. */
|
|
1040
|
+
enabled;
|
|
1041
|
+
local = (0, _qilin_agent_client_store.createSnapshotStore)(true);
|
|
1042
|
+
/**
|
|
1043
|
+
* @param scope - settings-owned namespace controller.
|
|
1044
|
+
*/
|
|
1045
|
+
constructor(scope) {
|
|
1046
|
+
this.scope = scope;
|
|
1047
|
+
this.enabled = scope.getSnapshot().mode === "memory" ? this.local : {
|
|
1048
|
+
getSnapshot: () => scope.getSnapshot().value?.enabled ?? false,
|
|
1049
|
+
subscribe: (listener) => {
|
|
1050
|
+
let previous = this.enabled.getSnapshot();
|
|
1051
|
+
return scope.subscribe(() => {
|
|
1052
|
+
const next = this.enabled.getSnapshot();
|
|
1053
|
+
if (next === previous) return;
|
|
1054
|
+
previous = next;
|
|
1055
|
+
listener();
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Persist a Host choice with ordered writes, or update the shared browser-local choice.
|
|
1062
|
+
* @param enabled - requested developer-tool mode.
|
|
1063
|
+
* @returns settlement after local publication or Host acceptance; rejects after a refused write recovers.
|
|
1064
|
+
*/
|
|
1065
|
+
async setEnabled(enabled) {
|
|
1066
|
+
if (this.scope.getSnapshot().mode === "memory") {
|
|
1067
|
+
this.local.set(enabled);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (!await this.scope.set("enabled", enabled)) throw new Error("Developer tools preference was not saved");
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
//#endregion
|
|
1074
|
+
//#region lib/types/developer-tools-settings.js
|
|
1075
|
+
/** Shared developer-tool preference stored by the Host user-settings document. */
|
|
1076
|
+
/** Namespace for developer UI and HTML preview capabilities. */
|
|
1077
|
+
const DEVELOPER_TOOLS_NAMESPACE = "ui-settings";
|
|
1078
|
+
/** New installations and missing values enable the full interface. */
|
|
1079
|
+
const DeveloperToolsSettingsFields = { enabled: Schema.boolean().default(true) };
|
|
1080
|
+
Schema.object({ enabled: DeveloperToolsSettingsFields["enabled"] });
|
|
1081
|
+
//#endregion
|
|
1082
|
+
//#region lib/types/client/config-form.js
|
|
1083
|
+
/** Shared entry values and ordered writes over the Host configuration mirror. */
|
|
1084
|
+
/**
|
|
1085
|
+
* One namespace's derived view over the shared describe mirror, plus that
|
|
1086
|
+
* namespace's serialized Host writes. Writes carry the latest known namespace
|
|
1087
|
+
* revision, fold their answers back into the mirror, and teardown waits for
|
|
1088
|
+
* the operation already crossing the wire.
|
|
1089
|
+
*/
|
|
1090
|
+
var ConfigFormController = class {
|
|
1091
|
+
ctx;
|
|
1092
|
+
spec;
|
|
1093
|
+
mirror;
|
|
1094
|
+
persistence;
|
|
1095
|
+
schema;
|
|
1096
|
+
store;
|
|
1097
|
+
tail = Promise.resolve();
|
|
1098
|
+
writeGeneration = 0;
|
|
1099
|
+
disposed = false;
|
|
1100
|
+
unsubscribe;
|
|
1101
|
+
/**
|
|
1102
|
+
* Revision answered by a superseded write still ahead of the mirror: the
|
|
1103
|
+
* mirror only folds the LATEST settlement in, so a queued successor takes
|
|
1104
|
+
* its fence from here first.
|
|
1105
|
+
*/
|
|
1106
|
+
pendingRevision;
|
|
1107
|
+
/**
|
|
1108
|
+
* @param ctx - the providing plugin's context, whose `remote.settings`
|
|
1109
|
+
* namespace carries this form's writes (reads ride the mirror).
|
|
1110
|
+
* @param spec - namespace identity and optional narrowing decoder.
|
|
1111
|
+
* @param mirror - the shared describe mirror this form derives from.
|
|
1112
|
+
* @param persistence - client-selected Host persistence; non-loopback pages may remain process-local.
|
|
1113
|
+
* @param schema - settings-owned schema operations.
|
|
1114
|
+
*/
|
|
1115
|
+
constructor(ctx, spec, mirror, persistence, schema) {
|
|
1116
|
+
this.ctx = ctx;
|
|
1117
|
+
this.spec = spec;
|
|
1118
|
+
this.mirror = mirror;
|
|
1119
|
+
this.persistence = persistence;
|
|
1120
|
+
this.schema = schema;
|
|
1121
|
+
this.store = (0, _qilin_agent_client_store.createSnapshotStore)({
|
|
1122
|
+
status: persistence === "host" ? "loading" : "unavailable",
|
|
1123
|
+
value: void 0,
|
|
1124
|
+
base: void 0,
|
|
1125
|
+
user: void 0,
|
|
1126
|
+
revision: void 0,
|
|
1127
|
+
writable: false,
|
|
1128
|
+
mode: persistence
|
|
1129
|
+
});
|
|
1130
|
+
if (persistence === "host") {
|
|
1131
|
+
this.unsubscribe = mirror.subscribe(() => {
|
|
1132
|
+
this.derive();
|
|
1133
|
+
});
|
|
1134
|
+
this.derive();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/** @returns the current sync snapshot (stable reference until the next change). */
|
|
1138
|
+
getSnapshot() {
|
|
1139
|
+
return this.store.getSnapshot();
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Observe snapshot replacements.
|
|
1143
|
+
* @param listener - invoked after each snapshot change.
|
|
1144
|
+
* @returns the disposer removing this listener.
|
|
1145
|
+
*/
|
|
1146
|
+
subscribe(listener) {
|
|
1147
|
+
return this.store.subscribe(listener);
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Queue one field write; see {@link ConfigForm.set} for the ordering,
|
|
1151
|
+
* revision, and recovery contract.
|
|
1152
|
+
* @param field - scalar field inside the namespace section.
|
|
1153
|
+
* @param value - JSON-shaped value selected by the user.
|
|
1154
|
+
* @returns whether the Host accepted the write, after any recovery read.
|
|
1155
|
+
*/
|
|
1156
|
+
set(field, value) {
|
|
1157
|
+
return this.mutate([{
|
|
1158
|
+
op: "set",
|
|
1159
|
+
path: [field],
|
|
1160
|
+
value
|
|
1161
|
+
}]);
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Queue one field clear; see {@link ConfigForm.unset} for the ordering,
|
|
1165
|
+
* revision, and recovery contract.
|
|
1166
|
+
* @param field - scalar field inside the namespace section.
|
|
1167
|
+
* @returns whether the Host accepted the clear, after any recovery read.
|
|
1168
|
+
*/
|
|
1169
|
+
unset(field) {
|
|
1170
|
+
return this.mutate([{
|
|
1171
|
+
op: "unset",
|
|
1172
|
+
path: [field]
|
|
1173
|
+
}]);
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Queue one atomic namespace mutation; see {@link ConfigForm.mutate}.
|
|
1177
|
+
* @param ops - ordered field operations copied when queued.
|
|
1178
|
+
* @param expectedRevision - optional fixed revision read by the domain editor.
|
|
1179
|
+
* @returns whether the Host accepted the mutation, after any recovery read.
|
|
1180
|
+
*/
|
|
1181
|
+
mutate(ops, expectedRevision) {
|
|
1182
|
+
const ownedOps = structuredClone(ops);
|
|
1183
|
+
const generation = ++this.writeGeneration;
|
|
1184
|
+
return this.enqueue(async () => {
|
|
1185
|
+
const revision = expectedRevision ?? this.pendingRevision ?? this.getSnapshot().revision;
|
|
1186
|
+
const response = await this.ctx.remote.settings.mutate(this.spec.namespace, ownedOps, revision);
|
|
1187
|
+
if (!response.ok) {
|
|
1188
|
+
await this.recover(generation);
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
if (this.disposed) return true;
|
|
1192
|
+
if (generation === this.writeGeneration) {
|
|
1193
|
+
this.pendingRevision = void 0;
|
|
1194
|
+
this.mirror.acceptView(response.value);
|
|
1195
|
+
} else this.pendingRevision = response.value.revision;
|
|
1196
|
+
return true;
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
/** Reload Host state for the latest failed write; superseded failures leave recovery to it. */
|
|
1200
|
+
async recover(generation) {
|
|
1201
|
+
if (this.disposed || generation !== this.writeGeneration) return;
|
|
1202
|
+
this.pendingRevision = void 0;
|
|
1203
|
+
await this.mirror.load();
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Stop queued operations, stop deriving, and wait for the current wire call
|
|
1207
|
+
* to settle.
|
|
1208
|
+
* @returns settlement after the controller reaches quiescence.
|
|
1209
|
+
*/
|
|
1210
|
+
async dispose() {
|
|
1211
|
+
this.disposed = true;
|
|
1212
|
+
this.writeGeneration += 1;
|
|
1213
|
+
this.unsubscribe?.();
|
|
1214
|
+
await this.tail;
|
|
1215
|
+
}
|
|
1216
|
+
enqueue(operation) {
|
|
1217
|
+
if (this.persistence === "memory" || this.disposed) return Promise.resolve(false);
|
|
1218
|
+
const task = this.tail.then(async () => {
|
|
1219
|
+
if (this.disposed) return false;
|
|
1220
|
+
return await operation();
|
|
1221
|
+
});
|
|
1222
|
+
this.tail = task.then(() => {}, () => {});
|
|
1223
|
+
return task;
|
|
1224
|
+
}
|
|
1225
|
+
derive() {
|
|
1226
|
+
if (this.disposed) return;
|
|
1227
|
+
const mirrored = this.mirror.getSnapshot();
|
|
1228
|
+
if (mirrored.view === void 0) return;
|
|
1229
|
+
const { writable } = mirrored.view;
|
|
1230
|
+
const view = mirrored.view.namespaces.find((candidate) => candidate.ns === this.spec.namespace);
|
|
1231
|
+
if (view === void 0) {
|
|
1232
|
+
this.store.update((draft) => {
|
|
1233
|
+
draft.status = "unavailable";
|
|
1234
|
+
draft.writable = writable;
|
|
1235
|
+
});
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
const decoded = this.decode(view);
|
|
1239
|
+
this.store.update((draft) => {
|
|
1240
|
+
draft.revision = view.revision;
|
|
1241
|
+
draft.base = view.base;
|
|
1242
|
+
draft.user = view.user;
|
|
1243
|
+
draft.writable = writable;
|
|
1244
|
+
if (decoded === void 0) return;
|
|
1245
|
+
draft.status = "ready";
|
|
1246
|
+
draft.value = decoded;
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
decode(view) {
|
|
1250
|
+
if (this.spec.decode !== void 0) return this.spec.decode(view.value);
|
|
1251
|
+
if (typeof view.value !== "object" || view.value === null || Array.isArray(view.value)) return void 0;
|
|
1252
|
+
let failure;
|
|
1253
|
+
try {
|
|
1254
|
+
failure = this.schema.validate(this.schema.rehydrate(view.schema), view.value);
|
|
1255
|
+
} catch (_malformedSchemaEnvelope) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
return failure === void 0 ? view.value : void 0;
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
/**
|
|
1262
|
+
* The settings domain's base service. Features that own a preference reach the
|
|
1263
|
+
* settings transport through this service rather than a shared function: the
|
|
1264
|
+
* client bundle purity gate forbids cross-plugin value imports and directs
|
|
1265
|
+
* cross-plugin collaboration through cordis services
|
|
1266
|
+
* (`packages/client/tsdown.client.ts`).
|
|
1267
|
+
*/
|
|
1268
|
+
var ConfigForms = class extends _qilin_agent_kylin.Service {
|
|
1269
|
+
forms = /* @__PURE__ */ new Map();
|
|
1270
|
+
/** Shared developer-tool preference owned by this settings provider. */
|
|
1271
|
+
developerTools;
|
|
1272
|
+
mirror;
|
|
1273
|
+
schema;
|
|
1274
|
+
persistence;
|
|
1275
|
+
/**
|
|
1276
|
+
* The PROVIDING fiber, kept because a Service reads `ctx` as its *consumer's*
|
|
1277
|
+
* fiber: letting a shared form write through the caller's context would make
|
|
1278
|
+
* every caller declare `remote.settings` in its own `inject`.
|
|
1279
|
+
*/
|
|
1280
|
+
owner;
|
|
1281
|
+
/**
|
|
1282
|
+
* @param ctx - the providing plugin's context.
|
|
1283
|
+
* @param config - the shared describe mirror every shared form derives from,
|
|
1284
|
+
* the settings-owned schema operations, and the Host persistence the provider
|
|
1285
|
+
* resolved from `remote.$host`.
|
|
1286
|
+
*/
|
|
1287
|
+
constructor(ctx, config) {
|
|
1288
|
+
super(ctx, "configForms");
|
|
1289
|
+
this.mirror = config.mirror;
|
|
1290
|
+
this.schema = config.schema;
|
|
1291
|
+
this.persistence = config.persistence;
|
|
1292
|
+
this.owner = ctx;
|
|
1293
|
+
this.developerTools = new DeveloperToolsPreference(this.get(DEVELOPER_TOOLS_NAMESPACE));
|
|
1294
|
+
ctx.effect(() => async () => {
|
|
1295
|
+
await Promise.all([...this.forms.values()].map((form) => form.dispose()));
|
|
1296
|
+
this.forms.clear();
|
|
1297
|
+
}, "ui-settings: configuration forms");
|
|
1298
|
+
}
|
|
1299
|
+
/**
|
|
1300
|
+
* The shared mirror's read/fold face for cross-namespace surfaces (schema
|
|
1301
|
+
* introspection, the served-namespace directory). Per-namespace consumers
|
|
1302
|
+
* use {@link get}; both derive from the same snapshot, so they can never
|
|
1303
|
+
* disagree about the document.
|
|
1304
|
+
* @returns the describe face over the shared mirror.
|
|
1305
|
+
*/
|
|
1306
|
+
describe() {
|
|
1307
|
+
return this.mirror;
|
|
1308
|
+
}
|
|
1309
|
+
/** Get the shared form values and write queue for one Host plugin entry.
|
|
1310
|
+
* @param entryId Unique Host plugin entry id.
|
|
1311
|
+
* @returns The entry's form, owned by this provider.
|
|
1312
|
+
*/
|
|
1313
|
+
get(entryId) {
|
|
1314
|
+
const existing = this.forms.get(entryId);
|
|
1315
|
+
if (existing !== void 0) return existing;
|
|
1316
|
+
const form = new ConfigFormController(this.owner, { namespace: entryId }, this.mirror, this.persistence, this.schema);
|
|
1317
|
+
this.forms.set(entryId, form);
|
|
1318
|
+
this.mirror.ensure();
|
|
1319
|
+
return form;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Keep a registration alive while the Host serves any of some namespaces:
|
|
1323
|
+
* `register` runs once one of them is in the describe mirror, and its
|
|
1324
|
+
* disposer runs when none is or when the returned disposer runs. A plugin
|
|
1325
|
+
* whose page edits a namespace another plugin owns registers the page
|
|
1326
|
+
* through this, so a deployment that never composed the owner shows no
|
|
1327
|
+
* trace of the page. The caller owns the returned disposer and wraps it in
|
|
1328
|
+
* `ctx.effect`; unlike {@link bind}, nothing is registered on the caller's
|
|
1329
|
+
* context here.
|
|
1330
|
+
* @param namespaces - the settings namespaces the registration follows.
|
|
1331
|
+
* @param register - registers the contribution, given every namespace the Host serves; returns its disposer.
|
|
1332
|
+
* @returns the disposer ending the watch and any live registration.
|
|
1333
|
+
*/
|
|
1334
|
+
whileServed(namespaces, register) {
|
|
1335
|
+
let off;
|
|
1336
|
+
const sync = () => {
|
|
1337
|
+
const served = new Set(this.mirror.getSnapshot().view?.namespaces.map((view) => view.ns) ?? []);
|
|
1338
|
+
const watched = namespaces.some((namespace) => served.has(namespace));
|
|
1339
|
+
if (watched && off === void 0) off = register(served);
|
|
1340
|
+
else if (!watched && off !== void 0) {
|
|
1341
|
+
off();
|
|
1342
|
+
off = void 0;
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
const unsubscribe = this.mirror.subscribe(sync);
|
|
1346
|
+
this.mirror.ensure();
|
|
1347
|
+
sync();
|
|
1348
|
+
return () => {
|
|
1349
|
+
unsubscribe();
|
|
1350
|
+
off?.();
|
|
1351
|
+
off = void 0;
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
//#endregion
|
|
1356
|
+
//#region lib/types/client/settings-mirror.js
|
|
1357
|
+
/**
|
|
1358
|
+
* Client mirror of the Host settings document: the one `settings.describe`
|
|
1359
|
+
* reader in the browser. Every settings consumer derives from this store —
|
|
1360
|
+
* shared entry forms through `ConfigForms.get`, cross-namespace
|
|
1361
|
+
* surfaces through the provider's shared describe face — so startup cost and
|
|
1362
|
+
* freshness are properties of this class, not of how many features own a
|
|
1363
|
+
* preference. The Host stays the fact source: the mirror re-reads on the
|
|
1364
|
+
* invalidations its owning plugin subscribes to and folds write answers in
|
|
1365
|
+
* through {@link SettingsDescribeMirror.acceptView}.
|
|
1366
|
+
*/
|
|
1367
|
+
/**
|
|
1368
|
+
* Serializes every Host `settings.describe` read behind one snapshot store.
|
|
1369
|
+
* Concurrent {@link load} calls fold into the in-flight read plus one rerun,
|
|
1370
|
+
* so an invalidation arriving mid-read is never lost and never duplicated.
|
|
1371
|
+
*/
|
|
1372
|
+
var SettingsDescribeMirror = class {
|
|
1373
|
+
ctx;
|
|
1374
|
+
persistence;
|
|
1375
|
+
store;
|
|
1376
|
+
inFlight;
|
|
1377
|
+
rerun = false;
|
|
1378
|
+
generation = 0;
|
|
1379
|
+
/**
|
|
1380
|
+
* @param ctx - the providing plugin's context, whose `remote.settings`
|
|
1381
|
+
* namespace answers the describe read.
|
|
1382
|
+
* @param persistence - client-selected Host persistence; non-loopback pages may remain process-local.
|
|
1383
|
+
*/
|
|
1384
|
+
constructor(ctx, persistence = "host") {
|
|
1385
|
+
this.ctx = ctx;
|
|
1386
|
+
this.persistence = persistence;
|
|
1387
|
+
this.store = (0, _qilin_agent_client_store.createSnapshotStore)({
|
|
1388
|
+
status: persistence === "host" ? "idle" : "unavailable",
|
|
1389
|
+
view: void 0,
|
|
1390
|
+
error: null
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
/** @returns the current sync snapshot (stable reference until the next change). */
|
|
1394
|
+
getSnapshot() {
|
|
1395
|
+
return this.store.getSnapshot();
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Observe snapshot replacements.
|
|
1399
|
+
* @param listener - invoked after each snapshot change.
|
|
1400
|
+
* @returns the disposer removing this listener.
|
|
1401
|
+
*/
|
|
1402
|
+
subscribe(listener) {
|
|
1403
|
+
return this.store.subscribe(listener);
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Refresh from the Host. A call during an in-flight read marks one rerun
|
|
1407
|
+
* after it settles instead of racing a second wire read.
|
|
1408
|
+
* @returns settlement after this call's freshness is reflected.
|
|
1409
|
+
*/
|
|
1410
|
+
load() {
|
|
1411
|
+
if (this.persistence === "memory") return Promise.resolve();
|
|
1412
|
+
if (this.inFlight !== void 0) {
|
|
1413
|
+
this.rerun = true;
|
|
1414
|
+
return this.inFlight;
|
|
1415
|
+
}
|
|
1416
|
+
const run = Promise.resolve().then(() => this.run());
|
|
1417
|
+
this.inFlight = run;
|
|
1418
|
+
return run;
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Resolve once an answer is held (or the mirror is terminally unavailable),
|
|
1422
|
+
* reading only from `idle`. The cheap idempotent entry for surfaces that
|
|
1423
|
+
* render on first use.
|
|
1424
|
+
* @returns settlement of the current or newly started read, if any.
|
|
1425
|
+
*/
|
|
1426
|
+
ensure() {
|
|
1427
|
+
if (this.persistence === "memory") return Promise.resolve();
|
|
1428
|
+
if (this.inFlight !== void 0) return this.inFlight;
|
|
1429
|
+
if (this.getSnapshot().status === "idle") return this.load();
|
|
1430
|
+
return Promise.resolve();
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Fold one write answer's namespace view into the held view without a wire
|
|
1434
|
+
* read, and invalidate any read still in flight. With no held document, the
|
|
1435
|
+
* answer is not published as a partial document; an in-flight read reruns so
|
|
1436
|
+
* it cannot publish a document fetched before the write committed.
|
|
1437
|
+
* @param view - the namespace view a settings write answered with.
|
|
1438
|
+
*/
|
|
1439
|
+
acceptView(view) {
|
|
1440
|
+
const before = this.store.getSnapshot();
|
|
1441
|
+
this.generation += 1;
|
|
1442
|
+
if (this.inFlight !== void 0) this.rerun = true;
|
|
1443
|
+
if (before.view === void 0) return;
|
|
1444
|
+
const namespaces = before.view.namespaces.some((row) => row.ns === view.ns) ? before.view.namespaces.map((row) => row.ns === view.ns ? view : row) : [...before.view.namespaces, view];
|
|
1445
|
+
this.store.set({
|
|
1446
|
+
...before,
|
|
1447
|
+
view: {
|
|
1448
|
+
...before.view,
|
|
1449
|
+
namespaces
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Convenience row lookup on the held view.
|
|
1455
|
+
* @param ns - namespace identity.
|
|
1456
|
+
* @returns the namespace view, or undefined while unanswered or unregistered.
|
|
1457
|
+
*/
|
|
1458
|
+
namespace(ns) {
|
|
1459
|
+
return this.store.getSnapshot().view?.namespaces.find((row) => row.ns === ns);
|
|
1460
|
+
}
|
|
1461
|
+
async run() {
|
|
1462
|
+
try {
|
|
1463
|
+
do {
|
|
1464
|
+
const before = this.store.getSnapshot();
|
|
1465
|
+
if (before.status === "idle") this.store.set({
|
|
1466
|
+
...before,
|
|
1467
|
+
status: "loading"
|
|
1468
|
+
});
|
|
1469
|
+
this.rerun = false;
|
|
1470
|
+
const generation = ++this.generation;
|
|
1471
|
+
let outcome;
|
|
1472
|
+
try {
|
|
1473
|
+
const response = await this.ctx.remote.settings.describe();
|
|
1474
|
+
outcome = response.ok ? { view: response.value } : { failure: response.error.message };
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
outcome = { failure: error instanceof Error ? error.message : String(error) };
|
|
1477
|
+
}
|
|
1478
|
+
if (generation !== this.generation) continue;
|
|
1479
|
+
if ("view" in outcome) this.store.set({
|
|
1480
|
+
status: "ready",
|
|
1481
|
+
view: outcome.view,
|
|
1482
|
+
error: null
|
|
1483
|
+
});
|
|
1484
|
+
else {
|
|
1485
|
+
const held = this.store.getSnapshot();
|
|
1486
|
+
this.store.set({
|
|
1487
|
+
status: held.view === void 0 ? "idle" : "ready",
|
|
1488
|
+
view: held.view,
|
|
1489
|
+
error: outcome.failure
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
} while (this.shouldRerun());
|
|
1493
|
+
} finally {
|
|
1494
|
+
this.inFlight = void 0;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
shouldRerun() {
|
|
1498
|
+
return this.rerun;
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
//#endregion
|
|
1502
|
+
//#region lib/types/client/index.js
|
|
1503
|
+
/**
|
|
1504
|
+
* Required services: the Remote namespace the mirror reads through and the
|
|
1505
|
+
* forwarded settings invalidation it refreshes on.
|
|
1506
|
+
*/
|
|
1507
|
+
const inject = ["remote", "remote.settings"];
|
|
1508
|
+
/**
|
|
1509
|
+
* Provide the shared configuration forms over one describe mirror, and keep
|
|
1510
|
+
* that mirror fresh on the two signals that can move the settings document: a
|
|
1511
|
+
* document commit and a (re)connect.
|
|
1512
|
+
*
|
|
1513
|
+
* Constructing the service in this plugin's fiber keeps its traced methods
|
|
1514
|
+
* bound to each consuming plugin's context.
|
|
1515
|
+
* @param ctx - client root context.
|
|
1516
|
+
*/
|
|
1517
|
+
function apply(ctx) {
|
|
1518
|
+
const schema = new SettingsSchemaService(ctx);
|
|
1519
|
+
const persistence = ctx.remote.$host.isLoopback ? "host" : "memory";
|
|
1520
|
+
const mirror = new SettingsDescribeMirror(ctx, persistence);
|
|
1521
|
+
ctx.effect(() => {
|
|
1522
|
+
const disposers = [ctx.remote.$on("settings/document-updated", () => {
|
|
1523
|
+
mirror.load();
|
|
1524
|
+
}), ctx.on("connection/reset", () => {
|
|
1525
|
+
mirror.load();
|
|
1526
|
+
})];
|
|
1527
|
+
mirror.ensure();
|
|
1528
|
+
return () => {
|
|
1529
|
+
for (const dispose of disposers) dispose();
|
|
1530
|
+
};
|
|
1531
|
+
}, "ui-settings: describe mirror invalidations");
|
|
1532
|
+
new ConfigForms(ctx, {
|
|
1533
|
+
mirror,
|
|
1534
|
+
schema,
|
|
1535
|
+
persistence
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
//#endregion
|
|
1539
|
+
exports.apply = apply;
|
|
1540
|
+
exports.inject = inject;
|
|
1541
|
+
return module.exports;
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
|
|
1545
|
+
//# sourceMappingURL=client.js.map
|