Создайте универсальное хранилище ключей для смарт-контрактов канистр на компьютере в Интернете.
Мне нравятся автономные веб-приложения, и большинство моих личных проектов с открытым исходным кодом, таких как 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 и получайте удовольствие 🤙.
Дальнейшее чтение
Хотите узнать больше о нашем проекте? Вот список сообщений в блоге, которые я опубликовал с тех пор, как мы начали проект с Интернет-компьютером:
- Утилиты TypeScript для Candid
- Пока, Amazon и Google, привет, Web 3.0
- Динамический импорт модулей ESM из CDN
- Интернет-компьютер: архитектура децентрализованной базы данных веб-приложения
- Singleton & Factory Patterns With TypeScript
- Хостинг в Интернете Компьютер
- Мы получили грант на перенос нашего веб-приложения на интернет-компьютер
Поддерживать связь
Чтобы следить за нашим приключением, вы можете пометить и посмотреть наш репозиторий GitHub ⭐️ и зарегистрироваться, чтобы присоединиться к списку бета-тестеров.
Заключение
Возможно, есть лучший способ реализовать параметры фильтрации, и я не уверен, что такая архитектура является современной (другие разработчики Motoko создают магазины рядом со своими канистрами?).
Тем не менее, он очень хорошо подходит для моих проектов, и, как я все еще учусь, его можно улучшить только со временем, потому что я переношу наш веб-редактор на Интернет-компьютер DFINITY и не собираюсь останавливаться в ближайшее время.
Бесконечность не предел!
Дэйвид
Вы можете связаться со мной в Твиттере или на моем сайте.
Попробуйте DeckDeckGo для ваших следующих слайдов.