Keyval это хранилище инстансов и данных для них, прямого доступа к инстансам он не даёт, можно лишь добавить либо удалить инстанс (есть отдельный апи для этого), поэтому возникает вопрос: как достать из кейвала какой-то элемент? Могут требоваться как данные из этого инстанса, так и апи для управления ним
Ответ на него — линзы
Допустим, есть иерархия — массив ресторанов, в каждом ресторане массив блюд, в каждом блюде массив добавок. Как достать конкретную добавку, имея лишь статически вызываемую функцию create и три стора $restaurant, $dish и $additive, которые хранят id элементов?
Линзы это апи чтобы погрузиться внутрь иерархии. Имея глубоко вложенный кейвал с кейвалами внутри, линзы дают возможность взять самый верхний, сказать: верни мне ресторан с ID из стора $restaurant, в нем прочитай поле dishes — это тоже кейвал, в dishes возьми инстанс с ID из стора $dish и так до тех пор пока не дойдёшь до какого-то окончательного юнита в модели. Если тебе нужен стор — ты получаешь на выходе стор, который будет трекать что находится по описанному тобой пути и соответственно обновляться. Если тебе нужен эвент — ты получишь эвент, и сможешь его вызывать как обычный эвент, а он сам погрузится в глубину иерахии по нужному пути и вызовет именно её эвент
Таким образом происходит работа с глубокими вложенностями не имея инстансов этой самой иерархии а лишь декларативно описывая адрес с которым ты хочешь работать
Эта линза написана для модели отражающей такую структуру данных:
type Restaurants = Array<{
name: string
dishes: Array<{
name: string
price: number
additives: Array<{
name: string
price: number
}>
}>
}>
Раскрытый вид модели, с описанием юнитов будет иметь такой вид
type Restaurants = Keyval<{
name: Store<string>
dishes: Keyval<{
name: Store<string>
price: Store<number>
additives: Keyval<{
name: Store<string>
price: Store<number>
}>
}>
}>
Функции dishes и additives создаются автоматически исходя из структуры кейвала, ведь она известна с момента создания кейвала. Эти функции-геттеры возвращают вложенные линзы до тех пор пока не будет прочитаны поля, возвращающие итоговые юниты, как .store, который в данном случае возвращает объект добавки целиком, насколько глубокая модель бы за ней не стояла
В случае с $additiveEntity мы получили объект добавки целиком, потому что обратились к полю .store сразу после чтения элемента кейвала additives, но можно читать и конкретные поля:
const $restaurantName = lens(restaurantsList, $restaurant)
.name
.store
const $dishPrice = lens(restaurantsList, $restaurant)
.dishes($dish)
.price
.store
const $additivePrice = lens(restaurantsList, $restaurant)
.dishes($dish)
.additives($additive)
.price
.store;
Эвенты читаются через линзы точно таким же образом, на выходе после чтения поля .event будет эвент, который при триггере пройдёт по всей иерархии и вызовет событие в конкретном инстансе
Допустим, мы расширяем модель списка ресторанов для управления его содержимым, редактированием цен в меню:
type Restaurants = Keyval<{
name: Store<string>
dishes: Keyval<{
name: Store<string>
price: Store<number>
changePrice: EventCallable<number>
additives: Keyval<{
name: Store<string>
price: Store<number>
changePrice: EventCallable<number>
}>
}>
}>
Тогда для изменения цены блюда или добавки, имея id ресторана и id блюда, через линзу можно получить эвент, который при вызове будет менять соответствующее поле в структуре данных:
const changeDishPrice = lens(restaurantsList, $restaurant)
.dishes($dish)
.changePrice
.event
const changeAdditivePrice = lens(restaurantsList, $restaurant)
.dishes($dish)
.additives($additive)
.changePrice
.event