@weasel-js/prefs 1.10.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/dist/{chunk-5KB3WBJZ.js → chunk-ASSX3JHY.js} +363 -46
- package/dist/chunk-ASSX3JHY.js.map +1 -0
- package/dist/index.d.ts +99 -3
- package/dist/index.js +172 -37
- package/dist/index.js.map +1 -1
- package/dist/{open-BEY-aOGe.d.ts → open-C83pU7Va.d.ts} +112 -10
- package/dist/react.d.ts +8 -3
- package/dist/react.js +11 -3
- package/dist/react.js.map +1 -1
- package/package.json +5 -4
- package/dist/chunk-5KB3WBJZ.js.map +0 -1
package/README.md
CHANGED
|
@@ -9,6 +9,22 @@ store.get('view.gridVisible');
|
|
|
9
9
|
store.set('view.gridVisible', false);
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
A schema is plain data, and `pref` writes it shorter:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
const SCHEMA = pref.group('Prefs', {
|
|
16
|
+
view: pref.group('View', {
|
|
17
|
+
gridVisible: pref.boolean('Grid', true),
|
|
18
|
+
gridGap: pref.number('Grid gap', 8, { min: 2, max: 64, unit: { scale: 1, suffix: 'px' } }),
|
|
19
|
+
mode: pref.enum('Mode', 'dark', ['dark', 'light']),
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Each builder returns what the literal would hold (`{ name, children }` for a
|
|
25
|
+
group, `{ kind, name, description, default, … }` for a leaf), so paths and
|
|
26
|
+
value types are read from it the same way, and the two can be mixed.
|
|
27
|
+
|
|
12
28
|
One record per leaf. A leaf with no record follows its schema default. Reads
|
|
13
29
|
repair stored values that no longer fit the schema without writing them back.
|
|
14
30
|
`migrations[i]` takes stored records from version `i` to `i + 1`.
|
|
@@ -35,3 +51,87 @@ draws it there. Put an unlimited setting at the end of the range it is nearest:
|
|
|
35
51
|
a 0–100% limit is off at 100, and a scale with no such end takes `endless`,
|
|
36
52
|
which adds a stop one step past the range that stores `Infinity` and reads the
|
|
37
53
|
leaf's `infinity` word.
|
|
54
|
+
|
|
55
|
+
## Split values
|
|
56
|
+
|
|
57
|
+
A leaf that lists `splitBy` may hold its value once per option of another
|
|
58
|
+
leaf, where it would hold one. The other leaf, the selector, is an ordinary
|
|
59
|
+
`enum` named by its path:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
'look.mode': { kind: 'enum', default: 'dark', options: [dark, light], … }
|
|
63
|
+
'gap': { kind: 'number', default: 4, splitBy: ['look.mode'], … }
|
|
64
|
+
|
|
65
|
+
store.split('gap', 'look.mode'); // holds { $by: 'look.mode', $of: { dark: 4, light: 4 } }
|
|
66
|
+
store.set('gap', 8); // writes the member the mode is set to
|
|
67
|
+
store.set('look.mode', 'light'); // a change to every leaf split by it
|
|
68
|
+
store.join('gap', 'look.mode'); // back to one value: the member the mode is set to
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The stored value is a locus, `{ $by, $of }`, and a member may itself be split
|
|
72
|
+
by a second selector. `store.get` reads the member the selectors are set to.
|
|
73
|
+
`store.get(path, { weights })` reads the alternatives folded by weight, which
|
|
74
|
+
is what a transition between two options shows on the way:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
store.get('gap', { weights: { 'look.mode': { dark: 0.25, light: 0.75 } } });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A number blends linearly and a color blends in OKLCH. Every other kind takes
|
|
81
|
+
its heaviest alternative, the first option on a tie; `blends` in the open
|
|
82
|
+
options says otherwise for a kind. `store.values({ at: { 'look.mode': 'light' } })`
|
|
83
|
+
is the whole tree as it would read with the selectors set as given.
|
|
84
|
+
|
|
85
|
+
`store.set(path, value, { at })` writes the member a stated choice names,
|
|
86
|
+
whatever the selectors are set to, and `store.loci()` is what every split leaf
|
|
87
|
+
holds, by path: one map until something changes. `usePrefsValues` returns
|
|
88
|
+
both shaped for `PrefsForm`, with `split` and `join` beside them:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
const { values, set, loci, split, join, setAt } = usePrefsValues(store);
|
|
92
|
+
<PrefsForm schema={schema} values={values} onChange={set}
|
|
93
|
+
loci={loci} onSplit={split} onJoin={join} onChangeAt={setAt} />
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A leaf's default may be split too. `defaultLocus` is what the leaf holds while
|
|
97
|
+
nothing is stored for it, and `default` stays what a member it leaves out
|
|
98
|
+
reads as:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
'gap': { kind: 'number', default: 4, splitBy: ['look.mode'],
|
|
102
|
+
defaultLocus: { $by: 'look.mode', $of: { dark: 4, light: 6 } }, … }
|
|
103
|
+
|
|
104
|
+
store.get('gap'); // 4, or 6 once the mode is light
|
|
105
|
+
store.set('gap', 8, { at: { 'look.mode': 'light' } }); // holds { dark: 4, light: 8 }
|
|
106
|
+
store.reset('gap'); // the split default again
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A selector must be a plain `enum`: not `clearable`, with no `encoding`, and
|
|
110
|
+
not split itself. The store warns of a `splitBy` that names anything else when
|
|
111
|
+
it opens.
|
|
112
|
+
|
|
113
|
+
## As CSS custom properties
|
|
114
|
+
|
|
115
|
+
`prefsCss(store)` writes the store's values as custom properties, and
|
|
116
|
+
`prefsCssAttributes(store)` gives the attributes that pick its blocks:
|
|
117
|
+
|
|
118
|
+
```css
|
|
119
|
+
:where(:root) {
|
|
120
|
+
--look-mode: dark;
|
|
121
|
+
--gap: 4px; /* a split value, at the member the selectors' defaults name */
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
:where(:root[data-look-mode='light']) {
|
|
125
|
+
--look-mode: light;
|
|
126
|
+
--gap: 6px;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Every leaf is set on the root. A block follows for each choice of options a
|
|
131
|
+
split value tells apart, and only those: a value that is one thing under
|
|
132
|
+
`light` at every density gets one block for `light`. Every block is inside
|
|
133
|
+
`:where()`, so a rule of the page's own wins, and blocks naming more selectors
|
|
134
|
+
come later. A number is written in its display unit where the unit's suffix is
|
|
135
|
+
a CSS unit, a boolean as `1` or `0`, an enum's option as an identifier, and a
|
|
136
|
+
string quoted; a leaf of any other kind is left out unless the `value` option
|
|
137
|
+
writes it. `root`, `property`, and `attribute` rename what it writes.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatUnit, unitScale } from '@weasel-js/quantity';
|
|
2
2
|
import { isPlainObject, createEmitter } from '@weasel-js/registry';
|
|
3
|
+
import { hexToRgba, interpolateSrgb, rgbaToHex } from '@weasel-js/paint';
|
|
3
4
|
import { openRecords, openRecordsSync } from '@weasel-js/storage';
|
|
4
5
|
|
|
5
6
|
// src/alias.ts
|
|
@@ -30,6 +31,20 @@ function isPrefLinearUnit(unit) {
|
|
|
30
31
|
return typeof unit.toDisplay !== "function";
|
|
31
32
|
}
|
|
32
33
|
var SHOWN = "shown";
|
|
34
|
+
function decimalPlaces(n) {
|
|
35
|
+
const text = String(n);
|
|
36
|
+
if (text.includes("e")) return void 0;
|
|
37
|
+
const dot = text.indexOf(".");
|
|
38
|
+
return dot === -1 ? 0 : text.length - dot - 1;
|
|
39
|
+
}
|
|
40
|
+
var DECIMAL_PLACES = 12;
|
|
41
|
+
function storedOf(shown, scale, offset) {
|
|
42
|
+
const raw = shown * scale + offset;
|
|
43
|
+
const [a, b, c] = [decimalPlaces(shown), decimalPlaces(scale), decimalPlaces(offset)];
|
|
44
|
+
if (a === void 0 || b === void 0 || c === void 0 || !Number.isFinite(raw)) return raw;
|
|
45
|
+
const places = Math.max(a + b, c);
|
|
46
|
+
return places > DECIMAL_PLACES ? raw : Number(raw.toFixed(places));
|
|
47
|
+
}
|
|
33
48
|
function resolvePrefUnit(unit) {
|
|
34
49
|
if (!isPrefLinearUnit(unit)) {
|
|
35
50
|
return { ...unit, format: unit.format ?? ((stored) => `${unit.toDisplay(stored)}${unit.suffix ?? ""}`) };
|
|
@@ -42,7 +57,7 @@ function resolvePrefUnit(unit) {
|
|
|
42
57
|
const shown = (stored - offset) / scale;
|
|
43
58
|
return round === void 0 ? shown : Math.round(shown * round) / round;
|
|
44
59
|
},
|
|
45
|
-
fromDisplay: (shown) => shown
|
|
60
|
+
fromDisplay: (shown) => storedOf(shown, scale, offset),
|
|
46
61
|
format: (stored) => `${formatUnit(stored, SHOWN, system, { precision: precision ?? 2, suffix: false })}${suffix ?? ""}`,
|
|
47
62
|
...suffix === void 0 ? {} : { suffix },
|
|
48
63
|
...accepts === void 0 ? {} : { accepts }
|
|
@@ -482,6 +497,181 @@ function checkPrefConditions(schema, over) {
|
|
|
482
497
|
} else group(schema, "");
|
|
483
498
|
return out;
|
|
484
499
|
}
|
|
500
|
+
function isPrefLocus(value) {
|
|
501
|
+
if (!isPlainObject(value)) return false;
|
|
502
|
+
const keys = Object.keys(value);
|
|
503
|
+
return keys.length === 2 && typeof value.$by === "string" && isPlainObject(value.$of);
|
|
504
|
+
}
|
|
505
|
+
function selectionWeights(selection) {
|
|
506
|
+
return Object.fromEntries(Object.entries(selection).map(([path, option]) => [path, { [option]: 1 }]));
|
|
507
|
+
}
|
|
508
|
+
var member = (locus, option, absent) => Object.hasOwn(locus.$of, option) ? locus.$of[option] : absent;
|
|
509
|
+
function weighLocus(stored, weights, absent) {
|
|
510
|
+
const out = [];
|
|
511
|
+
const walk = (value, weight) => {
|
|
512
|
+
if (!isPrefLocus(value)) {
|
|
513
|
+
out.push({ value, weight });
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
for (const [option, share] of Object.entries(weights[value.$by] ?? {})) {
|
|
517
|
+
if (share > 0) walk(member(value, option, absent), weight * share);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
walk(stored, 1);
|
|
521
|
+
return out;
|
|
522
|
+
}
|
|
523
|
+
function memberAt(stored, selection, absent) {
|
|
524
|
+
let value = stored;
|
|
525
|
+
while (isPrefLocus(value)) {
|
|
526
|
+
const option = selection[value.$by];
|
|
527
|
+
if (option === void 0) return absent;
|
|
528
|
+
value = member(value, option, absent);
|
|
529
|
+
}
|
|
530
|
+
return value;
|
|
531
|
+
}
|
|
532
|
+
function writeMember(stored, selection, next) {
|
|
533
|
+
if (!isPrefLocus(stored)) return next;
|
|
534
|
+
const option = selection[stored.$by];
|
|
535
|
+
if (option === void 0) return stored;
|
|
536
|
+
return { $by: stored.$by, $of: { ...stored.$of, [option]: writeMember(stored.$of[option], selection, next) } };
|
|
537
|
+
}
|
|
538
|
+
function splitLocus(stored, by, options, order) {
|
|
539
|
+
const across = (value) => ({ $by: by, $of: Object.fromEntries(options.map((option) => [option, value])) });
|
|
540
|
+
if (!isPrefLocus(stored)) return across(stored);
|
|
541
|
+
if (stored.$by === by) return stored;
|
|
542
|
+
if (order.indexOf(stored.$by) > order.indexOf(by)) return across(stored);
|
|
543
|
+
return mapMembers(stored, (inner) => splitLocus(inner, by, options, order));
|
|
544
|
+
}
|
|
545
|
+
function joinLocus(stored, by, selection, absent) {
|
|
546
|
+
if (!isPrefLocus(stored)) return stored;
|
|
547
|
+
if (stored.$by !== by) return mapMembers(stored, (inner) => joinLocus(inner, by, selection, absent));
|
|
548
|
+
const option = selection[by];
|
|
549
|
+
return option === void 0 ? absent : member(stored, option, absent);
|
|
550
|
+
}
|
|
551
|
+
function mapMembers(locus, fn) {
|
|
552
|
+
let out;
|
|
553
|
+
for (const [option, value] of Object.entries(locus.$of)) {
|
|
554
|
+
const next = fn(value);
|
|
555
|
+
if (next !== value) (out ??= { ...locus.$of })[option] = next;
|
|
556
|
+
}
|
|
557
|
+
return out ? { $by: locus.$by, $of: out } : locus;
|
|
558
|
+
}
|
|
559
|
+
function mapLocusMembers(stored, fn) {
|
|
560
|
+
return isPrefLocus(stored) ? mapMembers(stored, (inner) => mapLocusMembers(inner, fn)) : fn(stored);
|
|
561
|
+
}
|
|
562
|
+
function locusSelectors(stored) {
|
|
563
|
+
const out = /* @__PURE__ */ new Set();
|
|
564
|
+
const walk = (value) => {
|
|
565
|
+
if (!isPrefLocus(value)) return;
|
|
566
|
+
out.add(value.$by);
|
|
567
|
+
for (const inner of Object.values(value.$of)) walk(inner);
|
|
568
|
+
};
|
|
569
|
+
walk(stored);
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
function repairLocus(stored, allowed, optionsOf, repairMember) {
|
|
573
|
+
const BAD = /* @__PURE__ */ Symbol("bad");
|
|
574
|
+
const walk = (value, met) => {
|
|
575
|
+
if (!isPrefLocus(value)) return repairMember(value);
|
|
576
|
+
const options = allowed.includes(value.$by) && !met.includes(value.$by) ? optionsOf(value.$by) : void 0;
|
|
577
|
+
if (!options) return BAD;
|
|
578
|
+
const of = {};
|
|
579
|
+
for (const option of options) {
|
|
580
|
+
if (!Object.hasOwn(value.$of, option)) continue;
|
|
581
|
+
const read2 = walk(value.$of[option], [...met, value.$by]);
|
|
582
|
+
if (read2 === BAD) return BAD;
|
|
583
|
+
of[option] = read2;
|
|
584
|
+
}
|
|
585
|
+
return { $by: value.$by, $of: of };
|
|
586
|
+
};
|
|
587
|
+
const read = walk(stored, []);
|
|
588
|
+
return read === BAD ? void 0 : read;
|
|
589
|
+
}
|
|
590
|
+
var heaviest = (members) => members.reduce((most, m) => m.weight > most.weight ? m : most).value;
|
|
591
|
+
var blendNumber = (members, leaf) => {
|
|
592
|
+
let sum = 0;
|
|
593
|
+
let total = 0;
|
|
594
|
+
for (const { value, weight } of members) {
|
|
595
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return heaviest(members);
|
|
596
|
+
sum += value * weight;
|
|
597
|
+
total += weight;
|
|
598
|
+
}
|
|
599
|
+
return sum / total;
|
|
600
|
+
};
|
|
601
|
+
var blendColor = (members, leaf) => {
|
|
602
|
+
let mixed;
|
|
603
|
+
let total = 0;
|
|
604
|
+
for (const { value, weight } of members) {
|
|
605
|
+
let next;
|
|
606
|
+
try {
|
|
607
|
+
next = hexToRgba(value);
|
|
608
|
+
} catch {
|
|
609
|
+
return heaviest(members);
|
|
610
|
+
}
|
|
611
|
+
total += weight;
|
|
612
|
+
if (!mixed) {
|
|
613
|
+
mixed = next;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const t = weight / total;
|
|
617
|
+
const [r, g, b] = interpolateSrgb([mixed[0], mixed[1], mixed[2]], [next[0], next[1], next[2]], t, "oklch");
|
|
618
|
+
mixed = [r, g, b, mixed[3] + (next[3] - mixed[3]) * t];
|
|
619
|
+
}
|
|
620
|
+
return rgbaToHex(mixed);
|
|
621
|
+
};
|
|
622
|
+
var PREF_BLENDS = { number: blendNumber, color: blendColor };
|
|
623
|
+
function blendPref(leaf, members, blends) {
|
|
624
|
+
if (members.length === 0) return leaf.default;
|
|
625
|
+
if (members.length === 1) return members[0].value;
|
|
626
|
+
const blend = (blends && Object.hasOwn(blends, leaf.kind) ? blends[leaf.kind] : void 0) ?? (Object.hasOwn(PREF_BLENDS, leaf.kind) ? PREF_BLENDS[leaf.kind] : void 0) ?? heaviest;
|
|
627
|
+
return blend(members, leaf);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/splitCheck.ts
|
|
631
|
+
var refusal = (leaf) => {
|
|
632
|
+
if (!leaf) return ["no-leaf", "names no leaf"];
|
|
633
|
+
if (leaf.kind !== "enum") return ["not-enum", `names a ${leaf.kind} leaf, and only an enum has options to split by`];
|
|
634
|
+
const selector = leaf;
|
|
635
|
+
if (selector.encoding) return ["encoded", "names an enum with an encoding, whose stored value is not its option"];
|
|
636
|
+
if (selector.clearable) return ["clearable", "names a clearable enum, which may hold none of its options"];
|
|
637
|
+
if (selector.splitBy?.length) return ["split", "names a leaf that may itself be split"];
|
|
638
|
+
return void 0;
|
|
639
|
+
};
|
|
640
|
+
function prefSelectors(leaves) {
|
|
641
|
+
const out = /* @__PURE__ */ new Map();
|
|
642
|
+
for (const leaf of leaves.values()) {
|
|
643
|
+
for (const path of leaf.splitBy ?? []) {
|
|
644
|
+
const selector = leaves.get(path);
|
|
645
|
+
if (out.has(path) || refusal(selector)) continue;
|
|
646
|
+
out.set(path, selector.options.map((option) => option.value));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return out;
|
|
650
|
+
}
|
|
651
|
+
function checkPrefSplits(schema) {
|
|
652
|
+
const leaves = prefLeaves(schema);
|
|
653
|
+
const out = [];
|
|
654
|
+
for (const [at, leaf] of leaves) {
|
|
655
|
+
for (const path of locusSelectors(leaf.defaultLocus)) {
|
|
656
|
+
if (leaf.splitBy?.includes(path)) continue;
|
|
657
|
+
out.push({ code: "default", at, path, problem: `its defaultLocus is split by "${path}", which its splitBy does not list` });
|
|
658
|
+
}
|
|
659
|
+
if (!leaf.splitBy?.length) continue;
|
|
660
|
+
if (!prefHoldsValue(leaf)) {
|
|
661
|
+
out.push({ code: "valueless", at, problem: `a ${leaf.kind} leaf holds no value to split` });
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
leaf.splitBy.forEach((path, i) => {
|
|
665
|
+
if (leaf.splitBy.indexOf(path) !== i) {
|
|
666
|
+
out.push({ code: "twice", at, path, problem: `"${path}" is listed twice` });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
const why = refusal(leaves.get(path));
|
|
670
|
+
if (why) out.push({ code: why[0], at, path, problem: `"${path}" ${why[1]}` });
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return out;
|
|
674
|
+
}
|
|
485
675
|
|
|
486
676
|
// src/union.ts
|
|
487
677
|
function prefVariantOf(leaf, value) {
|
|
@@ -700,6 +890,85 @@ function watchPrefsVersion(cache, migrations) {
|
|
|
700
890
|
stopChanges();
|
|
701
891
|
};
|
|
702
892
|
}
|
|
893
|
+
|
|
894
|
+
// src/snapshot.ts
|
|
895
|
+
function readStored(source, leaf, stored) {
|
|
896
|
+
const repair = (value) => repairPrefValue(leaf, value, source.validators);
|
|
897
|
+
if (!leaf.splitBy?.length || !isPrefLocus(stored)) return repair(stored);
|
|
898
|
+
const read = repairLocus(stored, leaf.splitBy, (path) => source.selectors.get(path), repair);
|
|
899
|
+
return read === void 0 ? leaf.default : read;
|
|
900
|
+
}
|
|
901
|
+
function resolveHeld(source, leaf, held, weights) {
|
|
902
|
+
return isPrefLocus(held) ? blendPref(leaf, weighLocus(held, weights, leaf.default), source.blends) : held;
|
|
903
|
+
}
|
|
904
|
+
function createSnapshots(source) {
|
|
905
|
+
const { leaves, cache, selectors } = source;
|
|
906
|
+
let snapshot = null;
|
|
907
|
+
let previous = null;
|
|
908
|
+
const unsetLoci = /* @__PURE__ */ new Map();
|
|
909
|
+
const unsetHeld = (path, leaf) => {
|
|
910
|
+
if (leaf.defaultLocus === void 0) return leaf.default;
|
|
911
|
+
if (!unsetLoci.has(path)) unsetLoci.set(path, readStored(source, leaf, leaf.defaultLocus));
|
|
912
|
+
return unsetLoci.get(path);
|
|
913
|
+
};
|
|
914
|
+
const build = () => {
|
|
915
|
+
const held = /* @__PURE__ */ new Map();
|
|
916
|
+
const raw = /* @__PURE__ */ new Map();
|
|
917
|
+
const unset = /* @__PURE__ */ new Set();
|
|
918
|
+
for (const [path, leaf] of leaves) {
|
|
919
|
+
if (!cache.has(path)) {
|
|
920
|
+
held.set(path, unsetHeld(path, leaf));
|
|
921
|
+
unset.add(path);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const record = cache.get(path);
|
|
925
|
+
raw.set(path, record);
|
|
926
|
+
const same2 = previous?.raw.has(path) && previous.raw.get(path) === record;
|
|
927
|
+
held.set(path, same2 ? previous.held.get(path) : readStored(source, leaf, recordValue(record)));
|
|
928
|
+
}
|
|
929
|
+
previous = { raw, held };
|
|
930
|
+
const selection = {};
|
|
931
|
+
for (const path of selectors.keys()) selection[path] = held.get(path);
|
|
932
|
+
const weights = selectionWeights(selection);
|
|
933
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
934
|
+
const splitBy = /* @__PURE__ */ new Map();
|
|
935
|
+
const loci = /* @__PURE__ */ new Map();
|
|
936
|
+
const tree = {};
|
|
937
|
+
for (const [path, leaf] of leaves) {
|
|
938
|
+
const value = held.get(path);
|
|
939
|
+
if (isPrefLocus(value)) loci.set(path, value);
|
|
940
|
+
for (const selector of locusSelectors(value)) splitBy.set(selector, [...splitBy.get(selector) ?? [], path]);
|
|
941
|
+
const read = resolveHeld(source, leaf, value, weights);
|
|
942
|
+
byPath.set(path, read);
|
|
943
|
+
assignPrefValueAtPath(tree, path, read);
|
|
944
|
+
}
|
|
945
|
+
return { byPath, held, loci, selection, splitBy, tree, unset };
|
|
946
|
+
};
|
|
947
|
+
return {
|
|
948
|
+
current: () => snapshot ??= build(),
|
|
949
|
+
peek: () => snapshot,
|
|
950
|
+
drop: () => {
|
|
951
|
+
snapshot = null;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function weightsUnder(selectors, selection, given) {
|
|
956
|
+
const out = {};
|
|
957
|
+
for (const [path, options] of selectors) {
|
|
958
|
+
const own = Object.hasOwn(given, path) ? given[path] : void 0;
|
|
959
|
+
out[path] = own ? Object.fromEntries(options.filter((option) => (own[option] ?? 0) > 0).map((option) => [option, own[option]])) : { [selection[path]]: 1 };
|
|
960
|
+
}
|
|
961
|
+
return out;
|
|
962
|
+
}
|
|
963
|
+
function selectionAt(selectors, selection, at) {
|
|
964
|
+
const out = { ...selection };
|
|
965
|
+
for (const [path, option] of Object.entries(at)) {
|
|
966
|
+
if (selectors.get(path)?.includes(option)) out[path] = option;
|
|
967
|
+
}
|
|
968
|
+
return out;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// src/storeWrites.ts
|
|
703
972
|
var encodeInfinity = (leaf, value) => {
|
|
704
973
|
if (leaf.kind === "number") return value === Infinity || value === -Infinity ? String(value) : value;
|
|
705
974
|
if (leaf.kind === "list" && Array.isArray(value)) {
|
|
@@ -722,6 +991,58 @@ var encodeInfinity = (leaf, value) => {
|
|
|
722
991
|
}
|
|
723
992
|
return out ?? value;
|
|
724
993
|
};
|
|
994
|
+
function prefWrites({ leaves, cache, selectors, version, selection, warnIfClosed }) {
|
|
995
|
+
const unsetLocus = (leaf) => {
|
|
996
|
+
if (leaf.defaultLocus === void 0 || !leaf.splitBy?.length) return void 0;
|
|
997
|
+
return repairLocus(leaf.defaultLocus, leaf.splitBy, (by) => selectors.get(by), (member2) => encodeInfinity(leaf, member2));
|
|
998
|
+
};
|
|
999
|
+
const rawAt = (path, leaf) => cache.has(path) ? recordValue(cache.get(path)) : unsetLocus(leaf) ?? encodeInfinity(leaf, leaf.default);
|
|
1000
|
+
const store = (path, stored) => {
|
|
1001
|
+
cache.set(path, stored === void 0 ? void 0 : stampRecord(stored, version));
|
|
1002
|
+
};
|
|
1003
|
+
const splitOf = (path, by) => {
|
|
1004
|
+
warnIfClosed(path);
|
|
1005
|
+
const leaf = leaves.get(path);
|
|
1006
|
+
const options = selectors.get(by);
|
|
1007
|
+
if (leaf?.splitBy?.includes(by) && options) return { leaf, options };
|
|
1008
|
+
console.warn(`[prefs] "${path}" cannot be split by "${by}"; ignoring`);
|
|
1009
|
+
return void 0;
|
|
1010
|
+
};
|
|
1011
|
+
const set = (path, value, write) => {
|
|
1012
|
+
warnIfClosed(path);
|
|
1013
|
+
const leaf = leaves.get(path);
|
|
1014
|
+
if (!leaf) {
|
|
1015
|
+
console.warn(`[prefs] no leaf at "${path}"; ignoring the write`);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (leaf.splitBy?.length && isPrefLocus(value)) {
|
|
1019
|
+
store(path, mapLocusMembers(value, (member2) => encodeInfinity(leaf, member2)));
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const stored = encodeInfinity(leaf, value);
|
|
1023
|
+
const had = cache.has(path) ? recordValue(cache.get(path)) : unsetLocus(leaf);
|
|
1024
|
+
const intoMember = stored !== void 0 && leaf.splitBy?.length && isPrefLocus(had);
|
|
1025
|
+
const under2 = write?.at ? selectionAt(selectors, selection(), write.at) : selection();
|
|
1026
|
+
store(path, intoMember ? writeMember(had, under2, stored) : stored);
|
|
1027
|
+
};
|
|
1028
|
+
const split = (path, by) => {
|
|
1029
|
+
const found = splitOf(path, by);
|
|
1030
|
+
if (!found) return;
|
|
1031
|
+
const had = rawAt(path, found.leaf);
|
|
1032
|
+
const next = splitLocus(had, by, found.options, found.leaf.splitBy);
|
|
1033
|
+
if (next !== had) store(path, next);
|
|
1034
|
+
};
|
|
1035
|
+
const join = (path, by) => {
|
|
1036
|
+
const found = splitOf(path, by);
|
|
1037
|
+
if (!found) return;
|
|
1038
|
+
const had = rawAt(path, found.leaf);
|
|
1039
|
+
const next = joinLocus(had, by, selection(), encodeInfinity(found.leaf, found.leaf.default));
|
|
1040
|
+
if (next !== had) store(path, next);
|
|
1041
|
+
};
|
|
1042
|
+
return { set, split, join };
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/store.ts
|
|
725
1046
|
function storedRecords(cache) {
|
|
726
1047
|
const tree = {};
|
|
727
1048
|
const branches = new WeakSet([tree]);
|
|
@@ -746,37 +1067,14 @@ function storedRecords(cache) {
|
|
|
746
1067
|
}
|
|
747
1068
|
return tree;
|
|
748
1069
|
}
|
|
749
|
-
function createPrefsStore(schema, cache, validators, onClose, version = 0) {
|
|
1070
|
+
function createPrefsStore(schema, cache, validators, onClose, version = 0, blends) {
|
|
750
1071
|
const leaves = new Map([...prefLeaves(schema)].filter(([, leaf]) => prefHoldsValue(leaf)));
|
|
751
1072
|
for (const { field, at, problem } of checkPrefConditions(schema)) {
|
|
752
1073
|
console.warn(`[prefs] ${field} of ${at === "" ? "the schema" : `"${at}"`}: ${problem}`);
|
|
753
1074
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
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();
|
|
1075
|
+
for (const { at, problem } of checkPrefSplits(schema)) console.warn(`[prefs] splitBy of "${at}": ${problem}`);
|
|
1076
|
+
const source = { leaves, cache, selectors: prefSelectors(leaves), validators, blends };
|
|
1077
|
+
const { current, peek, drop } = createSnapshots(source);
|
|
780
1078
|
let storedTree = null;
|
|
781
1079
|
let changed = createEmitter();
|
|
782
1080
|
const stopCache = cache.subscribe((changes) => {
|
|
@@ -784,10 +1082,16 @@ function createPrefsStore(schema, cache, validators, onClose, version = 0) {
|
|
|
784
1082
|
if (records.length === 0) return;
|
|
785
1083
|
storedTree = null;
|
|
786
1084
|
const mine = records.filter((c) => leaves.has(c.name));
|
|
787
|
-
|
|
1085
|
+
const before = peek();
|
|
1086
|
+
if (mine.length > 0) drop();
|
|
788
1087
|
const now = current();
|
|
789
|
-
const
|
|
790
|
-
|
|
1088
|
+
const origins = new Map(mine.map((c) => [c.name, c.origin]));
|
|
1089
|
+
for (const c of mine) {
|
|
1090
|
+
for (const path of [...before?.splitBy.get(c.name) ?? [], ...now.splitBy.get(c.name) ?? []]) {
|
|
1091
|
+
if (!origins.has(path)) origins.set(path, c.origin);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
changed.emit([...origins].map(([path, origin]) => ({ path, value: now.byPath.get(path), origin })));
|
|
791
1095
|
});
|
|
792
1096
|
let closed = false;
|
|
793
1097
|
let warned = false;
|
|
@@ -798,17 +1102,19 @@ function createPrefsStore(schema, cache, validators, onClose, version = 0) {
|
|
|
798
1102
|
};
|
|
799
1103
|
return {
|
|
800
1104
|
schema,
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1105
|
+
...prefWrites({ leaves, cache, selectors: source.selectors, version, selection: () => current().selection, warnIfClosed }),
|
|
1106
|
+
get: (path, read) => {
|
|
1107
|
+
const now = current();
|
|
1108
|
+
if (!read?.weights) return now.byPath.get(path);
|
|
804
1109
|
const leaf = leaves.get(path);
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
const stored = encodeInfinity(leaf, value);
|
|
810
|
-
cache.set(path, stored === void 0 ? void 0 : stampRecord(stored, version));
|
|
1110
|
+
const weights = weightsUnder(source.selectors, now.selection, read.weights);
|
|
1111
|
+
return leaf ? resolveHeld(source, leaf, now.held.get(path), weights) : void 0;
|
|
811
1112
|
},
|
|
1113
|
+
locus: (path) => {
|
|
1114
|
+
const held = current().held.get(path);
|
|
1115
|
+
return isPrefLocus(held) ? held : void 0;
|
|
1116
|
+
},
|
|
1117
|
+
loci: () => current().loci,
|
|
812
1118
|
reset: (path) => {
|
|
813
1119
|
warnIfClosed(path);
|
|
814
1120
|
for (const p of leaves.keys()) {
|
|
@@ -816,7 +1122,18 @@ function createPrefsStore(schema, cache, validators, onClose, version = 0) {
|
|
|
816
1122
|
}
|
|
817
1123
|
},
|
|
818
1124
|
isSet: (path) => cache.has(path),
|
|
819
|
-
values: () =>
|
|
1125
|
+
values: (read) => {
|
|
1126
|
+
const now = current();
|
|
1127
|
+
if (!read?.at) return now.tree;
|
|
1128
|
+
const selection = selectionAt(source.selectors, now.selection, read.at);
|
|
1129
|
+
const weights = selectionWeights(selection);
|
|
1130
|
+
const tree = {};
|
|
1131
|
+
for (const [path, leaf] of leaves) {
|
|
1132
|
+
const set = source.selectors.has(path) ? selection[path] : resolveHeld(source, leaf, now.held.get(path), weights);
|
|
1133
|
+
assignPrefValueAtPath(tree, path, set);
|
|
1134
|
+
}
|
|
1135
|
+
return tree;
|
|
1136
|
+
},
|
|
820
1137
|
unset: () => current().unset,
|
|
821
1138
|
stored: () => storedTree ??= storedRecords(cache),
|
|
822
1139
|
subscribe: (listener) => changed.subscribe(listener),
|
|
@@ -844,11 +1161,11 @@ async function openPrefs(schema, options) {
|
|
|
844
1161
|
const cache = await openRecords({ storage: options.storage, prefix: options.prefix });
|
|
845
1162
|
const stop = watch(cache, options);
|
|
846
1163
|
if (runPrefsMigrations(cache, options.migrations ?? [])) await cache.flush();
|
|
847
|
-
return createPrefsStore(schema, cache, options.validators, stop, options.migrations?.length ?? 0);
|
|
1164
|
+
return createPrefsStore(schema, cache, options.validators, stop, options.migrations?.length ?? 0, options.blends);
|
|
848
1165
|
}
|
|
849
1166
|
function openPrefsSync(schema, options) {
|
|
850
1167
|
const { cache, stop } = openPrefsCacheSync(options);
|
|
851
|
-
return createPrefsStore(schema, cache, options.validators, stop, options.migrations?.length ?? 0);
|
|
1168
|
+
return createPrefsStore(schema, cache, options.validators, stop, options.migrations?.length ?? 0, options.blends);
|
|
852
1169
|
}
|
|
853
1170
|
function openPrefsCacheSync(options) {
|
|
854
1171
|
const cache = openRecordsSync({ storage: options.storage, prefix: options.prefix });
|
|
@@ -858,6 +1175,6 @@ function openPrefsCacheSync(options) {
|
|
|
858
1175
|
}
|
|
859
1176
|
var watch = (cache, options) => watchPrefsVersion(cache, options.migrations ?? []);
|
|
860
1177
|
|
|
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-
|
|
863
|
-
//# sourceMappingURL=chunk-
|
|
1178
|
+
export { PREF_BLENDS, PREF_KINDS, UNSEEN, VERSION_RECORD, checkPrefCondition, checkPrefConditions, checkPrefSplits, createPrefsStore, filterPrefSubtree, flattenPrefValues, isBuiltinPref, isPrefCondition, isPrefLeaf, isPrefLinearUnit, isPrefLocus, isPrefSection, locusSelectors, mapPrefConditionPaths, memberAt, openPrefs, openPrefsCacheSync, openPrefsSync, pairRowsOf, prefAliasName, prefAliasTarget, prefAliasedLeaf, prefConditionHolds, prefConditionPaths, prefConditionReadsItem, prefDisplayBounds, prefHoldsValue, prefIsEnabled, prefIsShown, prefLeafAt, prefLeafHolding, prefLeafHoldingIn, prefLeaves, prefSchemaHolding, prefSectionLeaves, prefSelectors, prefUnit, prefValueAtPath, prefVariantDefault, prefVariantOf, repairPrefValue, resolvePrefUnit, visiblePrefSubtree };
|
|
1179
|
+
//# sourceMappingURL=chunk-ASSX3JHY.js.map
|
|
1180
|
+
//# sourceMappingURL=chunk-ASSX3JHY.js.map
|