@weasel-js/prefs 1.9.3 → 1.10.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.
@@ -0,0 +1,863 @@
1
+ import { formatUnit, unitScale } from '@weasel-js/quantity';
2
+ import { isPlainObject, createEmitter } from '@weasel-js/registry';
3
+ import { openRecords, openRecordsSync } from '@weasel-js/storage';
4
+
5
+ // src/alias.ts
6
+ function prefAliasTarget(alias, leafAt) {
7
+ const passed = /* @__PURE__ */ new Set();
8
+ let path = alias.of;
9
+ for (; ; ) {
10
+ if (passed.has(path)) return void 0;
11
+ passed.add(path);
12
+ const leaf = leafAt(path);
13
+ if (leaf === void 0) return void 0;
14
+ if (leaf.kind !== "alias") return { path, leaf };
15
+ path = leaf.of;
16
+ }
17
+ }
18
+ function prefAliasName(alias, target) {
19
+ if (alias.name !== "") return alias.name;
20
+ return target === void 0 ? "Alias" : target.name;
21
+ }
22
+ function prefAliasedLeaf(alias, target) {
23
+ return {
24
+ ...target,
25
+ name: prefAliasName(alias, target),
26
+ description: alias.description === "" ? target.description : alias.description
27
+ };
28
+ }
29
+ function isPrefLinearUnit(unit) {
30
+ return typeof unit.toDisplay !== "function";
31
+ }
32
+ var SHOWN = "shown";
33
+ function resolvePrefUnit(unit) {
34
+ if (!isPrefLinearUnit(unit)) {
35
+ return { ...unit, format: unit.format ?? ((stored) => `${unit.toDisplay(stored)}${unit.suffix ?? ""}`) };
36
+ }
37
+ const { scale, offset = 0, precision, suffix, accepts } = unit;
38
+ const round = precision === void 0 ? void 0 : 10 ** precision;
39
+ const system = { base: "stored", units: { [SHOWN]: { factor: scale, offset } } };
40
+ return {
41
+ toDisplay: (stored) => {
42
+ const shown = (stored - offset) / scale;
43
+ return round === void 0 ? shown : Math.round(shown * round) / round;
44
+ },
45
+ fromDisplay: (shown) => shown * scale + offset,
46
+ format: (stored) => `${formatUnit(stored, SHOWN, system, { precision: precision ?? 2, suffix: false })}${suffix ?? ""}`,
47
+ ...suffix === void 0 ? {} : { suffix },
48
+ ...accepts === void 0 ? {} : { accepts }
49
+ };
50
+ }
51
+ function prefUnit(system, display, opts) {
52
+ let self;
53
+ try {
54
+ self = unitScale(system, display);
55
+ } catch (e) {
56
+ throw new Error(`prefUnit: ${e instanceof Error ? e.message : String(e)}`);
57
+ }
58
+ const suffix = opts?.suffix ?? display;
59
+ const accepts = { [suffix]: 1 };
60
+ for (const name of Object.keys(system.units)) {
61
+ const other = unitScale(system, name);
62
+ const factor = other.factor / self.factor;
63
+ const offset = (other.offset - self.offset) / self.factor;
64
+ accepts[name] = offset === 0 ? factor : { factor, offset };
65
+ }
66
+ return {
67
+ suffix,
68
+ scale: self.factor,
69
+ ...self.offset === 0 ? {} : { offset: self.offset },
70
+ accepts,
71
+ ...opts?.precision === void 0 ? {} : { precision: opts.precision }
72
+ };
73
+ }
74
+
75
+ // src/helpers.ts
76
+ function prefDisplayBounds(p) {
77
+ if (!p.unit) return { min: p.min, max: p.max, step: p.step ?? 1 };
78
+ const { toDisplay } = resolvePrefUnit(p.unit);
79
+ const lo = p.min === void 0 ? void 0 : toDisplay(p.min);
80
+ const hi = p.max === void 0 ? void 0 : toDisplay(p.max);
81
+ const flipped = lo !== void 0 && hi !== void 0 && lo > hi;
82
+ const step = p.step === void 0 ? 1 : Math.abs(toDisplay(p.step) - toDisplay(0)) || p.step;
83
+ return { min: flipped ? hi : lo, max: flipped ? lo : hi, step };
84
+ }
85
+ function isPrefLeaf(node) {
86
+ return "kind" in node;
87
+ }
88
+ var VALUELESS = /* @__PURE__ */ new Set(["action", "alias", "label", "separator"]);
89
+ function prefHoldsValue(leaf) {
90
+ return !VALUELESS.has(leaf.kind);
91
+ }
92
+ function isPrefSection(node) {
93
+ return !isPrefLeaf(node) && "members" in node;
94
+ }
95
+ function prefSectionLeaves(members) {
96
+ return Object.entries(members).flatMap(
97
+ ([key, child]) => isPrefLeaf(child) ? [[key, child]] : prefSectionLeaves(child.members)
98
+ );
99
+ }
100
+ function prefLeafAt(schema, path) {
101
+ if (isPrefSection(schema)) return prefSectionLeaves(schema.members).find(([key]) => key === path)?.[1];
102
+ let node = schema;
103
+ for (const seg of path.split(".")) {
104
+ if (isPrefLeaf(node) || !Object.hasOwn(node.children, seg)) return void 0;
105
+ node = node.children[seg];
106
+ }
107
+ return isPrefLeaf(node) ? node : void 0;
108
+ }
109
+ function prefValueAtPath(values, path) {
110
+ let cur = values;
111
+ for (const seg of path.split(".")) {
112
+ if (cur == null || typeof cur !== "object" || !Object.hasOwn(cur, seg)) return void 0;
113
+ cur = cur[seg];
114
+ }
115
+ return cur;
116
+ }
117
+ function visibleFields(fields, keepEmpty) {
118
+ const out = {};
119
+ for (const [key, child] of Object.entries(fields)) {
120
+ if (isPrefLeaf(child)) {
121
+ const kept = visiblePrefSubtree(child, false, keepEmpty);
122
+ if (kept) out[key] = kept;
123
+ continue;
124
+ }
125
+ const members = visibleFields(child.members, keepEmpty);
126
+ if (Object.keys(members).length > 0 || keepEmpty) out[key] = { ...child, members };
127
+ }
128
+ return out;
129
+ }
130
+ function visiblePrefSubtree(node, showHidden, keepEmpty = false) {
131
+ if (isPrefLeaf(node)) {
132
+ if (node.hidden && !showHidden) return null;
133
+ if (showHidden || node.kind !== "object") return node;
134
+ return { ...node, children: visibleFields(node.children, keepEmpty) };
135
+ }
136
+ const group = node;
137
+ const children = {};
138
+ for (const [key, child] of Object.entries(group.children)) {
139
+ const kept = visiblePrefSubtree(child, showHidden, keepEmpty);
140
+ if (kept) children[key] = kept;
141
+ }
142
+ if (Object.keys(children).length === 0 && !keepEmpty) return null;
143
+ return { ...node, children };
144
+ }
145
+ var VERSION_RECORD = "$version";
146
+ function prefLeaves(schema) {
147
+ const out = /* @__PURE__ */ new Map();
148
+ const walk = (group, prefix) => {
149
+ for (const [key, child] of Object.entries(group.children)) {
150
+ if (key.includes(".")) {
151
+ throw new Error(`[prefs] schema key "${key}" contains "."; group keys are path segments`);
152
+ }
153
+ if (prefix === "" && key === VERSION_RECORD) {
154
+ throw new Error(`[prefs] schema key "${VERSION_RECORD}" is reserved`);
155
+ }
156
+ const path = prefix === "" ? key : `${prefix}.${key}`;
157
+ if (isPrefLeaf(child)) out.set(path, child);
158
+ else walk(child, path);
159
+ }
160
+ };
161
+ walk(schema, "");
162
+ return out;
163
+ }
164
+ function setOwnPrefValue(tree, key, value) {
165
+ Object.defineProperty(tree, key, { value, writable: true, enumerable: true, configurable: true });
166
+ }
167
+ function assignPrefValueAtPath(root, path, value) {
168
+ const parts = path.split(".");
169
+ let cursor = root;
170
+ for (const part of parts.slice(0, -1)) {
171
+ if (!Object.hasOwn(cursor, part) || cursor[part] == null) setOwnPrefValue(cursor, part, {});
172
+ cursor = cursor[part];
173
+ }
174
+ setOwnPrefValue(cursor, parts[parts.length - 1], value);
175
+ }
176
+ function flattenPrefValues(schema, tree) {
177
+ const out = [];
178
+ for (const path of prefLeaves(schema).keys()) {
179
+ const value = prefValueAtPath(tree, path);
180
+ if (value !== void 0) out.push([path, value]);
181
+ }
182
+ return out;
183
+ }
184
+ function prefLeafMatches(pref, path, query) {
185
+ if (pref.name.toLowerCase().includes(query)) return true;
186
+ if (pref.description?.toLowerCase().includes(query)) return true;
187
+ return path.toLowerCase().includes(query);
188
+ }
189
+ function filterPrefSubtree(node, query, path = "", over) {
190
+ const q = query.trim().toLowerCase();
191
+ if (q === "") return node;
192
+ const within = over ?? (isPrefLeaf(node) ? void 0 : node);
193
+ const find = (p) => within && prefLeafAt(within, p);
194
+ if (isPrefLeaf(node)) {
195
+ const target = node.kind === "alias" ? prefAliasTarget(node, find) : void 0;
196
+ const drawn = target ? prefAliasedLeaf(node, target.leaf) : node;
197
+ return prefLeafMatches(drawn, path, q) ? node : null;
198
+ }
199
+ const group = node;
200
+ if (group.name.toLowerCase().includes(q)) return node;
201
+ const children = {};
202
+ for (const [key, child] of Object.entries(group.children)) {
203
+ const kept = filterPrefSubtree(child, q, path === "" ? key : `${path}.${key}`, within);
204
+ if (kept) children[key] = kept;
205
+ }
206
+ if (Object.keys(children).length === 0) return null;
207
+ return { ...node, children };
208
+ }
209
+ var ORDERINGS = ["above", "below", "atLeast", "atMost"];
210
+ var TEST_KEYS = /* @__PURE__ */ new Set(["path", "from", "is", "isNot", "in", ...ORDERINGS]);
211
+ var isValue = (v) => v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean";
212
+ function formOf(value) {
213
+ if (!isPlainObject(value)) return void 0;
214
+ const keys = Object.keys(value);
215
+ if (keys.length === 1 && (keys[0] === "all" || keys[0] === "any")) return Array.isArray(value[keys[0]]) ? keys[0] : void 0;
216
+ if (keys.length === 1 && keys[0] === "not") return "not";
217
+ if (typeof value.path !== "string" || !keys.every((key) => TEST_KEYS.has(key))) return void 0;
218
+ if (value.from !== void 0 && value.from !== "item") return void 0;
219
+ if (value.is !== void 0 && !isValue(value.is)) return void 0;
220
+ if (value.isNot !== void 0 && !isValue(value.isNot)) return void 0;
221
+ if (value.in !== void 0 && !(Array.isArray(value.in) && value.in.every(isValue))) return void 0;
222
+ return ORDERINGS.every((op) => value[op] === void 0 || typeof value[op] === "number") ? "test" : void 0;
223
+ }
224
+ function partsOf(condition, form) {
225
+ const held = condition;
226
+ return form === "test" ? [] : form === "not" ? [held.not] : held[form];
227
+ }
228
+ function isPrefCondition(value) {
229
+ const form = formOf(value);
230
+ return form !== void 0 && partsOf(value, form).every(isPrefCondition);
231
+ }
232
+ var same = (held, value) => value === null ? held == null : held === value;
233
+ function isSet(held) {
234
+ if (Array.isArray(held)) return held.length > 0;
235
+ if (isPlainObject(held)) return Object.keys(held).length > 0;
236
+ return Boolean(held);
237
+ }
238
+ function testHolds(test, held) {
239
+ const ops = Object.keys(test).filter((key) => key !== "path" && key !== "from" && test[key] !== void 0);
240
+ if (ops.length === 0) return isSet(held);
241
+ if (test.is !== void 0 && !same(held, test.is)) return false;
242
+ if (test.isNot !== void 0 && same(held, test.isNot)) return false;
243
+ if (test.in !== void 0 && !test.in.some((value) => same(held, value))) return false;
244
+ if (!ORDERINGS.some((op) => test[op] !== void 0)) return true;
245
+ if (typeof held !== "number") return false;
246
+ return (test.above === void 0 || held > test.above) && (test.below === void 0 || held < test.below) && (test.atLeast === void 0 || held >= test.atLeast) && (test.atMost === void 0 || held <= test.atMost);
247
+ }
248
+ function prefConditionHolds(condition, read) {
249
+ if ("all" in condition) return condition.all.every((part) => prefConditionHolds(part, read));
250
+ if ("any" in condition) return condition.any.some((part) => prefConditionHolds(part, read));
251
+ if ("not" in condition) return !prefConditionHolds(condition.not, read);
252
+ return testHolds(condition, read(condition.path, condition.from));
253
+ }
254
+ function prefIsShown(node, read) {
255
+ return node.showIf === void 0 || prefConditionHolds(node.showIf, read);
256
+ }
257
+ function prefIsEnabled(node, read) {
258
+ return node.enableIf === void 0 || prefConditionHolds(node.enableIf, read);
259
+ }
260
+ function prefConditionPaths(condition, from) {
261
+ const out = /* @__PURE__ */ new Set();
262
+ const walk = (c) => {
263
+ if ("all" in c) c.all.forEach(walk);
264
+ else if ("any" in c) c.any.forEach(walk);
265
+ else if ("not" in c) walk(c.not);
266
+ else if (c.from === from) out.add(c.path);
267
+ };
268
+ walk(condition);
269
+ return [...out];
270
+ }
271
+ function prefConditionReadsItem(condition) {
272
+ if ("all" in condition) return condition.all.some(prefConditionReadsItem);
273
+ if ("any" in condition) return condition.any.some(prefConditionReadsItem);
274
+ if ("not" in condition) return prefConditionReadsItem(condition.not);
275
+ return condition.from === "item";
276
+ }
277
+ function mapPrefConditionPaths(condition, move) {
278
+ const each = (parts) => {
279
+ const next = parts.map((part) => mapPrefConditionPaths(part, move));
280
+ return next.every((part, i) => part === parts[i]) ? parts : next;
281
+ };
282
+ if ("all" in condition) {
283
+ const all = each(condition.all);
284
+ return all === condition.all ? condition : { all };
285
+ }
286
+ if ("any" in condition) {
287
+ const any = each(condition.any);
288
+ return any === condition.any ? condition : { any };
289
+ }
290
+ if ("not" in condition) {
291
+ const not = mapPrefConditionPaths(condition.not, move);
292
+ return not === condition.not ? condition : { not };
293
+ }
294
+ const path = move(condition.path, condition.from);
295
+ return path === condition.path ? condition : { ...condition, path };
296
+ }
297
+
298
+ // src/schema.ts
299
+ var PREF_KINDS = {
300
+ number: true,
301
+ boolean: true,
302
+ string: true,
303
+ enum: true,
304
+ color: true,
305
+ paint: true,
306
+ object: true,
307
+ list: true,
308
+ map: true,
309
+ union: true,
310
+ field: true,
311
+ action: true,
312
+ alias: true,
313
+ label: true,
314
+ separator: true
315
+ };
316
+ function pairRowsOf(leaves) {
317
+ const rows = /* @__PURE__ */ new Map();
318
+ for (const [path, leaf] of leaves) {
319
+ if (!leaf.pair || rows.has(path)) continue;
320
+ const row = { key: path, label: leaf.pair.label ?? leaf.name };
321
+ rows.set(path, row);
322
+ const partners = typeof leaf.pair.with === "string" ? [leaf.pair.with] : leaf.pair.with;
323
+ for (const partner of partners) if (!rows.has(partner)) rows.set(partner, row);
324
+ }
325
+ return rows;
326
+ }
327
+ function isBuiltinPref(leaf) {
328
+ return Object.hasOwn(PREF_KINDS, leaf.kind);
329
+ }
330
+
331
+ // src/holding.ts
332
+ var UNSEEN = /* @__PURE__ */ Symbol("unseen");
333
+ var under = (at, key) => at === "" ? key : `${at}.${key}`;
334
+ function amongFields(fields, path, at) {
335
+ const exact = fields.find(([key]) => key === path);
336
+ if (exact) return { leaf: exact[1], ...at === void 0 ? {} : { within: at } };
337
+ const outer = fields.find(([key]) => path.startsWith(`${key}.`));
338
+ return outer && insideLeaf(outer[1], path.slice(outer[0].length + 1), under(at ?? "", outer[0]));
339
+ }
340
+ function insideLeaf(leaf, rest, at) {
341
+ if (leaf.kind === "object") return amongFields(prefSectionLeaves(leaf.children), rest, at);
342
+ if (leaf.kind === "union") {
343
+ const { tag, variants } = leaf;
344
+ if (rest === tag) {
345
+ const options = Object.keys(variants).map((value) => ({ value, label: variants[value].name }));
346
+ return { leaf: { kind: "enum", name: tag, description: "", default: void 0, options }, within: at };
347
+ }
348
+ for (const variant of Object.values(variants)) {
349
+ const found = amongFields(prefSectionLeaves(variant.children), rest, at);
350
+ if (found !== void 0) return found;
351
+ }
352
+ return void 0;
353
+ }
354
+ return !isBuiltinPref(leaf) || leaf.kind === "list" || leaf.kind === "map" || leaf.kind === "paint" ? UNSEEN : void 0;
355
+ }
356
+ function prefLeafHolding(schema, path) {
357
+ if (isPrefSection(schema)) return amongFields(prefSectionLeaves(schema.members), path, void 0);
358
+ const steps = path.split(".");
359
+ let node = schema;
360
+ for (let i = 0; i < steps.length; i++) {
361
+ if (isPrefLeaf(node)) return insideLeaf(node, steps.slice(i).join("."), steps.slice(0, i).join("."));
362
+ if (!Object.hasOwn(node.children, steps[i])) return void 0;
363
+ node = node.children[steps[i]];
364
+ }
365
+ return isPrefLeaf(node) ? { leaf: node } : void 0;
366
+ }
367
+ function prefSchemaHolding(schema, path, over) {
368
+ return over === void 0 || prefLeafHolding(schema, path) !== void 0 ? schema : over;
369
+ }
370
+ function prefLeafHoldingIn(item, path) {
371
+ return path === "" ? { leaf: item } : insideLeaf(item, path, "");
372
+ }
373
+
374
+ // src/conditionCheck.ts
375
+ var show = (value) => typeof value === "string" ? `'${value}'` : String(value);
376
+ function neverHolds(leaf, value) {
377
+ if (value === null || !isBuiltinPref(leaf)) return void 0;
378
+ switch (leaf.kind) {
379
+ case "number":
380
+ return typeof value === "number" ? void 0 : "it is a number";
381
+ case "boolean":
382
+ return typeof value === "boolean" ? void 0 : "it is a boolean";
383
+ case "string":
384
+ case "color":
385
+ case "field":
386
+ return typeof value === "string" ? void 0 : `it is a ${leaf.kind === "string" ? "string" : `${leaf.kind}, held as a string`}`;
387
+ case "enum": {
388
+ const { options } = leaf;
389
+ if (options.some((option) => option.value === value)) return void 0;
390
+ return `its options are ${options.map((option) => show(option.value)).join(", ")}`;
391
+ }
392
+ case "object":
393
+ return leaf.fromScalar ? void 0 : "it is an object";
394
+ default:
395
+ return `it is a ${leaf.kind}`;
396
+ }
397
+ }
398
+ function testProblems(test, schema, site, over) {
399
+ const { path } = test;
400
+ if (test.from === "item" && site.within === void 0) {
401
+ return [{ code: "no-item", path, problem: `"${path}" is read from an entry, and this is inside no list or map` }];
402
+ }
403
+ const found = leafProblems(test, schema, site, over);
404
+ if (test.in?.length !== 0) return found;
405
+ return [...found, { code: "never", path, problem: `"${path}" is one of nothing, which never holds` }];
406
+ }
407
+ function leafProblems(test, schema, site, over) {
408
+ const { path } = test;
409
+ const inEntry = test.from === "item";
410
+ const held = inEntry ? prefLeafHoldingIn(site.within.item, path) : prefLeafHolding(prefSchemaHolding(schema, path, over), path);
411
+ if (held === void 0) return [{ code: "no-leaf", path, problem: `no leaf holds a value at "${path}"${inEntry ? " in the entry" : ""}` }];
412
+ if (held === UNSEEN) return [];
413
+ const { leaf } = held;
414
+ const named = inEntry ? path === "" ? "the entry" : `the entry's "${path}"` : `"${path}"`;
415
+ if (!prefHoldsValue(leaf)) return [{ code: "no-value", path, problem: `${named} is a${leaf.kind === "separator" || leaf.kind === "label" ? "" : "n"} ${leaf.kind}, which holds no value` }];
416
+ const out = [];
417
+ const compared = [test.is, test.isNot, ...test.in ?? []].filter((value) => value !== void 0);
418
+ for (const value of compared) {
419
+ const why = neverHolds(leaf, value);
420
+ if (why !== void 0) out.push({ code: "value", path, problem: `${named} never holds ${show(value)}: ${why}` });
421
+ }
422
+ const ordering = ORDERINGS.find((op) => test[op] !== void 0);
423
+ if (ordering !== void 0 && isBuiltinPref(leaf) && leaf.kind !== "number") {
424
+ out.push({ code: "value", path, problem: `${named} is a${leaf.kind === "object" || leaf.kind === "enum" ? "n" : ""} ${leaf.kind}, and "${ordering}" compares numbers` });
425
+ }
426
+ return out;
427
+ }
428
+ function checkPrefCondition(condition, schema, site = {}, over) {
429
+ if (site.itemShowIf && site.within) {
430
+ const { kind } = site.within;
431
+ const problem = `a ${kind}'s item is drawn with each entry and is never hidden: put the condition on the entry's fields, or on the ${kind}`;
432
+ return [{ code: "item-show", problem }, ...checkPrefCondition(condition, schema, { within: site.within }, over)];
433
+ }
434
+ const form = formOf(condition);
435
+ if (form === void 0) {
436
+ const shown = typeof condition === "function" ? "a function" : JSON.stringify(condition);
437
+ return [{ code: "malformed", problem: `not a condition: ${shown}` }];
438
+ }
439
+ if (form === "test") return testProblems(condition, schema, site, over);
440
+ return partsOf(condition, form).flatMap((part) => checkPrefCondition(part, schema, site, over));
441
+ }
442
+ function checkPrefConditions(schema, over) {
443
+ const out = [];
444
+ const own = (node, at, within, isItem = false) => {
445
+ for (const field of ["showIf", "enableIf"]) {
446
+ if (node[field] === void 0) continue;
447
+ const site = { ...within ? { within } : {}, ...isItem && field === "showIf" ? { itemShowIf: true } : {} };
448
+ for (const problem of checkPrefCondition(node[field], schema, site, over)) out.push({ ...problem, at, field });
449
+ }
450
+ };
451
+ const under2 = (at, key) => at === "" ? key : `${at}.${key}`;
452
+ const leaf = (node, at, within, isItem = false) => {
453
+ own(node, at, within, isItem);
454
+ if (node.kind === "object") members(node.children, at, true, within);
455
+ else if (node.kind === "list" || node.kind === "map") {
456
+ const holder = node;
457
+ if (holder.item !== void 0) leaf(holder.item, under2(at, "item"), holder, true);
458
+ } else if (node.kind === "union") {
459
+ for (const [key, variant] of Object.entries(node.variants)) leaf(variant, under2(at, key), within);
460
+ }
461
+ };
462
+ const members = (held, at, nested, within) => {
463
+ for (const [key, child] of Object.entries(held)) {
464
+ const here = nested ? under2(at, key) : key;
465
+ if (isPrefLeaf(child)) leaf(child, here, within);
466
+ else {
467
+ own(child, here, within);
468
+ members(child.members, nested ? at : "", nested, within);
469
+ }
470
+ }
471
+ };
472
+ const group = (node, at) => {
473
+ own(node, at, void 0);
474
+ for (const [key, child] of Object.entries(node.children)) {
475
+ if (isPrefLeaf(child)) leaf(child, under2(at, key), void 0);
476
+ else group(child, under2(at, key));
477
+ }
478
+ };
479
+ if (isPrefSection(schema)) {
480
+ own(schema, "", void 0);
481
+ members(schema.members, "", false, void 0);
482
+ } else group(schema, "");
483
+ return out;
484
+ }
485
+
486
+ // src/union.ts
487
+ function prefVariantOf(leaf, value) {
488
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
489
+ const key = value[leaf.tag];
490
+ if (typeof key !== "string" || !Object.hasOwn(leaf.variants, key)) return void 0;
491
+ return [key, leaf.variants[key]];
492
+ }
493
+ function prefVariantDefault(leaf, key) {
494
+ const held = leaf.variants[key]?.default;
495
+ const fields = held !== null && typeof held === "object" ? held : {};
496
+ return { ...fields, [leaf.tag]: key };
497
+ }
498
+ function repairPrefValue(leaf, stored, validators) {
499
+ if (leaf.kind === "number") {
500
+ if (stored === "Infinity") stored = Infinity;
501
+ else if (stored === "-Infinity") stored = -Infinity;
502
+ }
503
+ const validate = validators?.[leaf.kind];
504
+ if (validate) {
505
+ try {
506
+ const out = validate(stored, leaf);
507
+ return out === void 0 ? leaf.default : out;
508
+ } catch {
509
+ return leaf.default;
510
+ }
511
+ }
512
+ switch (leaf.kind) {
513
+ case "number":
514
+ return repairNumber(leaf, stored);
515
+ case "boolean":
516
+ return leaf.encoding || typeof stored === "boolean" ? stored : leaf.default;
517
+ case "string":
518
+ case "color":
519
+ case "field":
520
+ return typeof stored === "string" ? stored : leaf.default;
521
+ case "enum": {
522
+ const e = leaf;
523
+ if (e.encoding) return stored;
524
+ return e.options.some((o) => o.value === stored) ? stored : leaf.default;
525
+ }
526
+ case "object":
527
+ if (isPlainObject(stored)) return repairFields(leaf, stored, validators);
528
+ return leaf.fromScalar ? stored : leaf.default;
529
+ case "map":
530
+ return repairMap(leaf, stored, validators);
531
+ case "union": {
532
+ const variant = prefVariantOf(leaf, stored);
533
+ return variant ? repairFields(variant[1], stored, validators) : leaf.default;
534
+ }
535
+ case "list":
536
+ return repairList(leaf, stored, validators);
537
+ default:
538
+ return stored;
539
+ }
540
+ }
541
+ function repairFields(leaf, stored, validators) {
542
+ let out;
543
+ for (const [key, field] of prefSectionLeaves(leaf.children)) {
544
+ if (!Object.hasOwn(stored, key) || stored[key] === void 0) continue;
545
+ const read = repairPrefValue(field, stored[key], validators);
546
+ if (read !== stored[key]) (out ??= { ...stored })[key] = read;
547
+ }
548
+ return out ?? stored;
549
+ }
550
+ function repairMap(leaf, stored, validators) {
551
+ if (!isPlainObject(stored)) return leaf.default;
552
+ const held = stored;
553
+ let out;
554
+ for (const [key, value] of Object.entries(held)) {
555
+ const read = repairPrefValue(leaf.item, value, validators);
556
+ if (read !== value) (out ??= { ...held })[key] = read;
557
+ }
558
+ return out ?? stored;
559
+ }
560
+ function repairList(leaf, stored, validators) {
561
+ if (!Array.isArray(stored)) return leaf.default;
562
+ if (leaf.minItems !== void 0 && stored.length < leaf.minItems) return leaf.default;
563
+ const kept = leaf.maxItems !== void 0 && stored.length > leaf.maxItems ? stored.slice(0, leaf.maxItems) : stored;
564
+ const read = kept.map((entry) => repairPrefValue(leaf.item, entry, validators));
565
+ return kept === stored && read.every((entry, i) => entry === stored[i]) ? stored : read;
566
+ }
567
+ function repairNumber(leaf, stored) {
568
+ if (typeof stored !== "number" || Number.isNaN(stored)) return leaf.default;
569
+ const { endless } = leaf;
570
+ if (stored === Infinity && (endless === "max" || endless === "both")) return stored;
571
+ if (stored === -Infinity && (endless === "min" || endless === "both")) return stored;
572
+ let v = stored;
573
+ if (leaf.min !== void 0 && v < leaf.min) v = leaf.min;
574
+ if (leaf.max !== void 0 && v > leaf.max) v = leaf.max;
575
+ return Number.isFinite(v) ? v : leaf.default;
576
+ }
577
+
578
+ // src/stamp.ts
579
+ var isVersion = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
580
+ function stampRecord(value, at) {
581
+ return { $at: at, value };
582
+ }
583
+ function isStampedRecord(record) {
584
+ if (record === null || typeof record !== "object") return false;
585
+ const keys = Object.keys(record);
586
+ return keys.length === 2 && Object.hasOwn(record, "value") && isVersion(record.$at);
587
+ }
588
+ function readRecord(record, unstamped) {
589
+ return isStampedRecord(record) ? { at: record.$at, value: record.value, stamped: true } : { at: unstamped, value: record, stamped: false };
590
+ }
591
+ function recordValue(record) {
592
+ return isStampedRecord(record) ? record.value : record;
593
+ }
594
+
595
+ // src/migrate.ts
596
+ var isVersion2 = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
597
+ function runPrefsMigrations(cache, migrations, { stampAll = true } = {}) {
598
+ const target = migrations.length;
599
+ const raw = cache.get(VERSION_RECORD);
600
+ if (cache.has(VERSION_RECORD) && !isVersion2(raw)) {
601
+ console.warn(
602
+ `[prefs] "${cache.prefix}" has an unreadable ${VERSION_RECORD} (${JSON.stringify(raw)}); opening read-only`
603
+ );
604
+ cache.stopWriting();
605
+ return false;
606
+ }
607
+ const stored = cache.has(VERSION_RECORD) ? raw : 0;
608
+ const held = cache.entries().filter(([name]) => name !== VERSION_RECORD).map(([name, record]) => ({ name, ...readRecord(record, Math.min(stored, target)) }));
609
+ const newest = Math.max(stored, ...held.map((r) => r.at));
610
+ if (newest > target) {
611
+ console.warn(
612
+ `[prefs] "${cache.prefix}" was written at version ${newest}, newer than this build's ${target}; opening read-only`
613
+ );
614
+ cache.stopWriting();
615
+ return false;
616
+ }
617
+ if (!cache.writable) return false;
618
+ const at = new Map(held.filter((r) => r.at < target).map((r) => [r.name, r.at]));
619
+ const records = new Map(held.filter((r) => at.has(r.name)).map((r) => [r.name, structuredClone(r.value)]));
620
+ for (let v = Math.min(target, ...at.values()); v < target; v++) {
621
+ const due = new Map([...records].filter(([name]) => at.get(name) <= v));
622
+ const before = [...due.keys()];
623
+ try {
624
+ migrations[v](due);
625
+ } catch (error) {
626
+ console.warn(
627
+ `[prefs] migration ${v} \u2192 ${v + 1} for "${cache.prefix}" threw; opening read-only on the unmigrated records`,
628
+ error
629
+ );
630
+ cache.stopWriting();
631
+ return false;
632
+ }
633
+ for (const name of before) if (!due.has(name)) records.delete(name);
634
+ for (const [name, value] of due) {
635
+ records.set(name, value);
636
+ at.set(name, v + 1);
637
+ }
638
+ }
639
+ let wrote = false;
640
+ for (const r of held) {
641
+ if (at.has(r.name)) {
642
+ if (!records.has(r.name)) cache.delete(r.name);
643
+ wrote = true;
644
+ } else if (stampAll && !r.stamped) {
645
+ cache.set(r.name, stampRecord(r.value, target));
646
+ wrote = true;
647
+ }
648
+ }
649
+ for (const [name, value] of records) {
650
+ if (value === void 0) cache.delete(name);
651
+ else cache.set(name, stampRecord(value, target));
652
+ }
653
+ if (stored !== target) {
654
+ cache.set(VERSION_RECORD, target);
655
+ wrote = true;
656
+ }
657
+ return wrote;
658
+ }
659
+ function watchPrefsVersion(cache, migrations) {
660
+ const target = migrations.length;
661
+ let versioned = cache.has(VERSION_RECORD);
662
+ let stopped = false;
663
+ const migrate = () => {
664
+ const cleared = versioned && !cache.has(VERSION_RECORD);
665
+ if (cleared) cache.set(VERSION_RECORD, target);
666
+ else if (!runPrefsMigrations(cache, migrations, { stampAll: false })) return;
667
+ versioned = true;
668
+ void cache.flush();
669
+ };
670
+ void cache.whenRead().then(() => {
671
+ if (!stopped && cache.writable) migrate();
672
+ });
673
+ const heard = (version) => {
674
+ if (!isVersion2(version)) {
675
+ console.warn(
676
+ `[prefs] "${cache.prefix}" has an unreadable ${VERSION_RECORD} (${JSON.stringify(version)}) from another writer; no longer persisting`
677
+ );
678
+ cache.stopWriting();
679
+ } else if (version > target) {
680
+ console.warn(
681
+ `[prefs] "${cache.prefix}" was migrated to version ${version} by another writer; this build knows ${target}; no longer persisting`
682
+ );
683
+ cache.stopWriting();
684
+ }
685
+ };
686
+ const stopOverruled = cache.onOverruled((changes) => {
687
+ for (const [name, value] of changes) if (name === VERSION_RECORD && value !== void 0) heard(value);
688
+ });
689
+ const stopChanges = cache.subscribe((changes) => {
690
+ const remote = changes.filter((change) => change.origin === "remote");
691
+ for (const change of remote) {
692
+ if (change.name === VERSION_RECORD && change.value !== void 0) heard(change.value);
693
+ }
694
+ if (remote.length > 0 && cache.writable) migrate();
695
+ versioned ||= cache.has(VERSION_RECORD);
696
+ });
697
+ return () => {
698
+ stopped = true;
699
+ stopOverruled();
700
+ stopChanges();
701
+ };
702
+ }
703
+ var encodeInfinity = (leaf, value) => {
704
+ if (leaf.kind === "number") return value === Infinity || value === -Infinity ? String(value) : value;
705
+ if (leaf.kind === "list" && Array.isArray(value)) {
706
+ const { item } = leaf;
707
+ return value.map((entry) => encodeInfinity(item, entry));
708
+ }
709
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
710
+ const held = value;
711
+ if (leaf.kind === "map") {
712
+ const { item } = leaf;
713
+ return Object.fromEntries(Object.entries(held).map(([key, entry]) => [key, encodeInfinity(item, entry)]));
714
+ }
715
+ const fields = leaf.kind === "union" ? prefVariantOf(leaf, held)?.[1] : leaf;
716
+ if (fields?.kind !== "object") return value;
717
+ let out;
718
+ for (const [key, field] of prefSectionLeaves(fields.children)) {
719
+ if (!Object.hasOwn(held, key)) continue;
720
+ const encoded = encodeInfinity(field, held[key]);
721
+ if (encoded !== held[key]) (out ??= { ...held })[key] = encoded;
722
+ }
723
+ return out ?? value;
724
+ };
725
+ function storedRecords(cache) {
726
+ const tree = {};
727
+ const branches = new WeakSet([tree]);
728
+ for (const [name, record] of cache.entries()) {
729
+ if (name === VERSION_RECORD) continue;
730
+ const value = recordValue(record);
731
+ const parts = name.split(".");
732
+ let cursor = tree;
733
+ for (const part of parts.slice(0, -1)) {
734
+ const next = cursor[part];
735
+ if (typeof next === "object" && next !== null && branches.has(next)) {
736
+ cursor = next;
737
+ } else {
738
+ const branch = {};
739
+ branches.add(branch);
740
+ setOwnPrefValue(cursor, part, branch);
741
+ cursor = branch;
742
+ }
743
+ }
744
+ const last = parts[parts.length - 1];
745
+ if (!branches.has(cursor[last])) setOwnPrefValue(cursor, last, value);
746
+ }
747
+ return tree;
748
+ }
749
+ function createPrefsStore(schema, cache, validators, onClose, version = 0) {
750
+ const leaves = new Map([...prefLeaves(schema)].filter(([, leaf]) => prefHoldsValue(leaf)));
751
+ for (const { field, at, problem } of checkPrefConditions(schema)) {
752
+ console.warn(`[prefs] ${field} of ${at === "" ? "the schema" : `"${at}"`}: ${problem}`);
753
+ }
754
+ let snapshot = null;
755
+ let previous = null;
756
+ const build = () => {
757
+ const byPath = /* @__PURE__ */ new Map();
758
+ const raw = /* @__PURE__ */ new Map();
759
+ const unset = /* @__PURE__ */ new Set();
760
+ const tree = {};
761
+ for (const [path, leaf] of leaves) {
762
+ const has = cache.has(path);
763
+ let value;
764
+ if (!has) {
765
+ value = leaf.default;
766
+ unset.add(path);
767
+ } else {
768
+ const record = cache.get(path);
769
+ const stored = recordValue(record);
770
+ raw.set(path, record);
771
+ value = previous?.raw.has(path) && previous.raw.get(path) === record ? previous.byPath.get(path) : repairPrefValue(leaf, stored, validators);
772
+ }
773
+ byPath.set(path, value);
774
+ assignPrefValueAtPath(tree, path, value);
775
+ }
776
+ previous = { raw, byPath };
777
+ return { byPath, tree, unset };
778
+ };
779
+ const current = () => snapshot ??= build();
780
+ let storedTree = null;
781
+ let changed = createEmitter();
782
+ const stopCache = cache.subscribe((changes) => {
783
+ const records = changes.filter((c) => c.name !== VERSION_RECORD);
784
+ if (records.length === 0) return;
785
+ storedTree = null;
786
+ const mine = records.filter((c) => leaves.has(c.name));
787
+ if (mine.length > 0) snapshot = null;
788
+ const now = current();
789
+ const out = mine.map((c) => ({ path: c.name, value: now.byPath.get(c.name), origin: c.origin }));
790
+ changed.emit(out);
791
+ });
792
+ let closed = false;
793
+ let warned = false;
794
+ const warnIfClosed = (path) => {
795
+ if (!closed || warned) return;
796
+ warned = true;
797
+ console.warn(`[prefs] write to a closed store${path === void 0 ? "" : ` at "${path}"`}; it will not persist`);
798
+ };
799
+ return {
800
+ schema,
801
+ get: (path) => current().byPath.get(path),
802
+ set: (path, value) => {
803
+ warnIfClosed(path);
804
+ const leaf = leaves.get(path);
805
+ if (!leaf) {
806
+ console.warn(`[prefs] no leaf at "${path}"; ignoring the write`);
807
+ return;
808
+ }
809
+ const stored = encodeInfinity(leaf, value);
810
+ cache.set(path, stored === void 0 ? void 0 : stampRecord(stored, version));
811
+ },
812
+ reset: (path) => {
813
+ warnIfClosed(path);
814
+ for (const p of leaves.keys()) {
815
+ if (path === void 0 || p === path || p.startsWith(`${path}.`)) cache.delete(p);
816
+ }
817
+ },
818
+ isSet: (path) => cache.has(path),
819
+ values: () => current().tree,
820
+ unset: () => current().unset,
821
+ stored: () => storedTree ??= storedRecords(cache),
822
+ subscribe: (listener) => changed.subscribe(listener),
823
+ get writable() {
824
+ return !closed && cache.writable;
825
+ },
826
+ get closed() {
827
+ return closed;
828
+ },
829
+ read: () => cache.read(),
830
+ whenRead: () => cache.whenRead(),
831
+ flush: () => cache.flush(),
832
+ close: async () => {
833
+ closed = true;
834
+ onClose?.();
835
+ stopCache();
836
+ changed = createEmitter();
837
+ await cache.close();
838
+ }
839
+ };
840
+ }
841
+
842
+ // src/open.ts
843
+ async function openPrefs(schema, options) {
844
+ const cache = await openRecords({ storage: options.storage, prefix: options.prefix });
845
+ const stop = watch(cache, options);
846
+ if (runPrefsMigrations(cache, options.migrations ?? [])) await cache.flush();
847
+ return createPrefsStore(schema, cache, options.validators, stop, options.migrations?.length ?? 0);
848
+ }
849
+ function openPrefsSync(schema, options) {
850
+ const { cache, stop } = openPrefsCacheSync(options);
851
+ return createPrefsStore(schema, cache, options.validators, stop, options.migrations?.length ?? 0);
852
+ }
853
+ function openPrefsCacheSync(options) {
854
+ const cache = openRecordsSync({ storage: options.storage, prefix: options.prefix });
855
+ const stop = watch(cache, options);
856
+ if (runPrefsMigrations(cache, options.migrations ?? [])) void cache.flush();
857
+ return { cache, stop };
858
+ }
859
+ var watch = (cache, options) => watchPrefsVersion(cache, options.migrations ?? []);
860
+
861
+ export { PREF_KINDS, UNSEEN, VERSION_RECORD, checkPrefCondition, checkPrefConditions, createPrefsStore, filterPrefSubtree, flattenPrefValues, isBuiltinPref, isPrefCondition, isPrefLeaf, isPrefLinearUnit, isPrefSection, mapPrefConditionPaths, openPrefs, openPrefsCacheSync, openPrefsSync, pairRowsOf, prefAliasName, prefAliasTarget, prefAliasedLeaf, prefConditionHolds, prefConditionPaths, prefConditionReadsItem, prefDisplayBounds, prefHoldsValue, prefIsEnabled, prefIsShown, prefLeafAt, prefLeafHolding, prefLeafHoldingIn, prefLeaves, prefSchemaHolding, prefSectionLeaves, prefUnit, prefValueAtPath, prefVariantDefault, prefVariantOf, repairPrefValue, resolvePrefUnit, visiblePrefSubtree };
862
+ //# sourceMappingURL=chunk-5KB3WBJZ.js.map
863
+ //# sourceMappingURL=chunk-5KB3WBJZ.js.map