Создайте универсальное хранилище ключей для смарт-контрактов канистр на компьютере в Интернете.

Мне нравятся автономные веб-приложения, и большинство моих личных проектов с открытым исходным кодом, таких как DeckDeckGo или Tie Tracker, используют этот подход.

В этих двух конкретных приложениях я использую idb-keyval, чтобы облегчить взаимодействие с IndexedDB через API, подобный keyval.

Вот почему в последней итерации нашей миграции на Интернет-компьютер DFINITY я разработал универсальное хранилище для смарт-контрактов канистр в Motoko, которое также поддерживает данные с ключами и значениями.

Магазин

Моя цель — иметь возможность многократно использовать один и тот же магазин в контейнерах и проектах. Если бы один из моих акторов содержал данные разных типов, например автомобили и овощи, я хотел бы повторно использовать тот же помощник, который инкапсулирует данные и предоставляет такие функции, как: put, get, delete и list.

Таким образом, хранилище, которое я разработал, представляет собой не что иное, как универсальный класс, который использует HashMap для сохранения с текстовыми ключами (тип Text).

import Text "mo:base/Text";
import HashMap "mo:base/HashMap";
import Iter "mo:base/Iter";
import Array "mo:base/Array";

module {
    public class DataStore<T>() {
        private var data: HashMap.HashMap<Text, T> = 
                HashMap.HashMap<Text, T>(10, Text.equal, Text.hash);
    }
}

Поместить, получить и удалить

Функции, которые изменяют состояние, в основном применяют изменения непосредственно к HashMap, за исключением операции удаления, которую я расширил с помощью getter, хотя delete ничего не делает, если ключ не существует. Я подумал, что иногда может быть интересно вернуть значение удаленного ключа.

public func put(key: Text, value: T) {
    data.put(key, value);
};

public func get(key: Text): ?T {
    return data.get(key);
};

public func del(key: Text): ?T {
    let entry: ?T = get(key);

    switch (entry) {
        case (?entry) {
            data.delete(key);
        };
        case (null) {};
    };

    return entry;
};

Список

Получение списка всех записей хранилища также было бы не более чем прямым запросом HashMap, если бы не возможность фильтрации данных. Действительно, может быть интересно искать только те ключи, которые начинаются с определенного префикса или содержат его.

Сначала я определил новый тип DataFilter для фильтра и реализовал эффективные функции фильтрации, которые признают необязательный характер этих параметров.

public type DataFilter = {
    startsWith: ?Text;
    contains: ?Text;
};

private func keyStartsWith(key: Text, startsWith: ?Text): Bool {
    switch (startsWith) {
        case null {
            return true;
        };
        case (?startsWith) {
            return Text.startsWith(key, #text startsWith);
        };
    };
};

private func keyContains(key: Text, contains: ?Text): Bool {
    switch (contains) {
        case null {
            return true;
        };
        case (?contains) {
            return Text.contains(key, #text contains);
        };
    };
};

Вышеупомянутые функции возвращают true, если фильтры не определены, при условии, что undefined означает игнорировать параметр. Вероятно, есть лучший способ реализовать такое условие в Motoko, но я еще не так хорошо разбираюсь в нем, как в других языках, таких как TypeScript. Если вы хотите улучшить решение, дерзайте, пришлите мне Pull Request!

Наконец, я реализовал саму функцию list, которая либо возвращает все записи, либо применяет фильтр в соответствии с логикой and.

public func list(filter: ?DataFilter): [(Text, T)] {
    let entries: Iter.Iter<(Text, T)> = data.entries();

    switch (filter) {
        case null {
            return Iter.toArray(entries);
        };
        case (?filter) {
            let keyValues: [(Text, T)] = Iter.toArray(entries);

            let {startsWith; contains} = filter;

            let values: [(Text, T)] = 
                        Array.mapFilter<(Text, T), (Text, T)>
            (keyValues, func ((key: Text, value: T)) : ?(Text, T) {
                if (keyStartsWith(key, startsWith) and 
                    keyContains(key, contains)) {
                    return ?(key, value);
                };

                return null;
            });


            return values;
        };
    };
};

Обновления

Чтобы сохранить состояние канистр при обновлении, системные перехватчики до обновления и после обновления могут быть реализованы для переменных, которые не являются стабильными по умолчанию. Чтобы предвидеть такой процесс, я также добавил в магазин две последние функции.

public func preupgrade(): HashMap.HashMap<Text, T> {
    return data;
};

public func postupgrade(stableData: [(Text, T)]) {
    data := HashMap.fromIter<Text, T>(stableData.vals(), 10, Text.equal, Text.hash);
};

Применение

Чтобы продемонстрировать использование такого универсального хранилища в акторе, мы создаем пустой контейнер, определяющий тип объекта для хранения, например Car .

Мы import хелпер и объявляем оба объекта, которые будем использовать. Сам store и стабильный entries для поддержания состояния при обновлениях.

import Iter "mo:base/Iter";

import DataStore "./store";

actor Test {

    type Car = {
        name: Text;
        manufacturer: Text;
    };

    private let store: DataStore.DataStore<Car> = 
                       DataStore.DataStore<Car>();

    private stable var entries : [(Text, Car)] = [];

    system func preupgrade() {
        entries := Iter.toArray(store.preupgrade().entries());
    };

    system func postupgrade() {
        store.postupgrade(entries);
        entries := [];
    };
};

Как только они определены, мы открываем функции, которые изменяют состояние и связывают их с хранилищем.

public query func get(key: Text) : async (?Car) {
    let entry: ?Car = store.get(key);
    return entry;
};

public func set(key: Text, data: Car) : async () {
    store.put(key, data);
};

public func del(key: Text) : async () {
    let entry: ?Car = store.del(key);
};

Наконец, мы подключаем последний фрагмент кода, функцию, которая выводит список и фильтрует записи.

public query func get(key: Text) : async (?Car) {
    let entry: ?Car = store.get(key);
    return entry;
};

И вуаля, с помощью нескольких строк кода мы реализовали простой смарт-контракт keyval canister, в котором хранятся наши данные 🥳.

Детская площадка

Хотите поиграть с предыдущим примером и сохранить? Загляните на эту Игровую площадку Motoko и получайте удовольствие 🤙.

Дальнейшее чтение

Хотите узнать больше о нашем проекте? Вот список сообщений в блоге, которые я опубликовал с тех пор, как мы начали проект с Интернет-компьютером:

Поддерживать связь

Чтобы следить за нашим приключением, вы можете пометить и посмотреть наш репозиторий GitHub ⭐️ и зарегистрироваться, чтобы присоединиться к списку бета-тестеров.

Заключение

Возможно, есть лучший способ реализовать параметры фильтрации, и я не уверен, что такая архитектура является современной (другие разработчики Motoko создают магазины рядом со своими канистрами?).

Тем не менее, он очень хорошо подходит для моих проектов, и, как я все еще учусь, его можно улучшить только со временем, потому что я переношу наш веб-редактор на Интернет-компьютер DFINITY и не собираюсь останавливаться в ближайшее время.

Бесконечность не предел!

Дэйвид

Вы можете связаться со мной в Твиттере или на моем сайте.

Попробуйте DeckDeckGo для ваших следующих слайдов.