Fetch

Fetch 模块负责通知相关用户。总共有 4 种 Canister :

Root Fetch Canister 的功能相对比较简单,它负责创建和管理所有种类的 Fetch Canister 。

Post Fetch Canister 负责通知粉丝更新帖子。Comment Fetch Canister 和 Like Fetch Canister 负责推送评论和点赞的通知。

因为这个模块里的代码重复度较高,我们只详细讲 Root Fetch Canister 、Post Fetch Canister 和 Like Fetch Canister 的代码。


rootFetch.mo

image-20240128223418061

首先还是定义好类型、变量。

actor class RootFetch(
    userCanister: Principal,
) = this {

    type RootFeedActor = Types.RootFeedActor;
    type PostFetchActor = Types.PostFetchActor;
    type CommentFetchActor = Types.CommentFetchActor;
    type LikeFetchActor = Types.LikeFetchActor;

    stable let T_CYCLES = 1_000_000_000_000;
    stable var rootFeedCanister = Principal.fromText("2vxsx-fae");
    stable var postFetchCanisterIndex: Nat = 0;
    stable var commentFetchCanisterIndex: Nat = 0;
    stable var likeFetchCanisterIndex: Nat = 0;

    // 创建三个TrieMap,分别存储帖子、评论和点赞
    // 这样可以方便地管理和检索相关数据,例如查找某个帖子的评论或点赞信息
    
    // 它的类型是一个元组数组,每个元组包含一个自然数(Nat)和一个Principal
    stable var postFetchMapEntries: [(Nat, Principal)] = [];
    // 从postFetchMapEntries数组的值中构建Map,使用Nat.equal处理键的相等性,使用Hash.hash进行哈希
    let postFetchMap = TrieMap.fromEntries<Nat, Principal>(postFetchMapEntries.vals(), Nat.equal, Hash.hash);

    stable var commentFetchMapEntries: [(Nat, Principal)] = [];
    let commentFetchMap = TrieMap.fromEntries<Nat, Principal>(commentFetchMapEntries.vals(), Nat.equal, Hash.hash);
    
    stable var likeFetchMapEntries: [(Nat, Principal)] = [];
    let likeFetchMap = TrieMap.fromEntries<Nat, Principal>(likeFetchMapEntries.vals(), Nat.equal, Hash.hash);    
};

我们通过初始化函数在创建 Root Fetch Canister 之后创建子 Canister ,建立了用户 Feed 、帖子、评论和点赞之间的联系。

public shared({caller}) func init(
    _rootFeedCanister: Principal,
    _initPostFetchCanister: Principal,
    _initCommentFetchCanister: Principal,
    _initLikeFetchCanister: Principal
): async () {
    rootFeedCanister := _rootFeedCanister;

    // 定义三个map存储不同类型的 fetch canister
    postFetchMap.put(postFetchCanisterIndex, _initPostFetchCanister);
    commentFetchMap.put(commentFetchCanisterIndex, _initCommentFetchCanister);
    likeFetchMap.put(likeFetchCanisterIndex, _initLikeFetchCanister);

    // 还定义了三个索引变量,用于为每个 fetch canister 生成一个唯一的索引
    postFetchCanisterIndex += 1;
    commentFetchCanisterIndex += 1;
    likeFetchCanisterIndex += 1;

    // 用actor函数创建四个actor对象
    let rootFeedActor: RootFeedActor = actor(Principal.toText(_rootFeedCanister));
    let _postFetchActor: PostFetchActor = actor(Principal.toText(_initPostFetchCanister));
    let _commentFetchActor: CommentFetchActor = actor(Principal.toText(_initCommentFetchCanister));
    let _likeFetchActor: LikeFetchActor = actor(Principal.toText(_initLikeFetchCanister));
    let _allUserFeedCanister = await rootFeedActor.getAllUserFeedCanister();

    // 调用三个Fetch的initUserToFeed方法,传入用户 feed canister 列表,在这些Fetch里初始化用户和feed的关系
    // 用assert确认三个initUserToFeed调用都成功执行
    assert(await _postFetchActor.initUserToFeed(_allUserFeedCanister));
    assert(await _commentFetchActor.initUserToFeed(_allUserFeedCanister));
    assert(await _likeFetchActor.initUserToFeed(_allUserFeedCanister));
};

接下来是创建 Post Fetch Canister 的函数:

创建一个新 Post Fetch Canister ,并将新创建的 Canister ID 存储在 Map 中。它还与 Root Feed Canister 进行一些交互。

public shared({caller}) func createPostFetchCanister(): async Principal {
    // 给每个Canister添加 4T Cycles
    Cycles.add(4 * T_CYCLES);
    // 调用PostFetch模块的构造函数来创建一个新的 PostFetch Canister
    let _canister = await PostFetch.PostFetch();
    // 获取新创建的 Canister id
    let _canisterId = Principal.fromActor(_canister);
    // 将新Canister的Principal放入postFetchMap中
    postFetchMap.put(postFetchCanisterIndex, _canisterId);
    // 索引+1
    postFetchCanisterIndex += 1;

    // postFetch : initUserToFeed
    // 使用assert检查rootFeedCanister是否不是匿名
    assert(not Principal.isAnonymous(rootFeedCanister));
    // 通过Principal.toText将rootFeedCanister转换为文本,然后用actor关键字创建RootFeedActor
    let rootFeedActor: RootFeedActor = actor(Principal.toText(rootFeedCanister));
    // 使用assert检查并确保调用_canister的initUserToFeed方法,并传递所有用户 Feed Canister 的Principal列表
    assert(await _canister.initUserToFeed((await rootFeedActor.getAllUserFeedCanister())));

    // 返回新创建的 Canister id
    _canisterId
};

创建负责通知评论和点赞的 Fetch Canister ,基本和上面创建 Post Fetch Canister 一样:

public shared({caller}) func createCommentFetchCanister(): async Principal {
    Cycles.add(4 * T_CYCLES);
    let _canister = await CommentFetch.CommentFetch(
        userCanister
    );
    let _canisterId = Principal.fromActor(_canister);
    commentFetchMap.put(commentFetchCanisterIndex, _canisterId);
    commentFetchCanisterIndex += 1;

    // initUserToFeed
    assert(not Principal.isAnonymous(rootFeedCanister));
    let rootFeedActor: RootFeedActor = actor(Principal.toText(rootFeedCanister));
    assert(await _canister.initUserToFeed((await rootFeedActor.getAllUserFeedCanister())));

    _canisterId
};

public shared({caller}) func createLikeFetchCanister(): async Principal {
    Cycles.add(4 * T_CYCLES);
    let _canister = await LikeFetch.LikeFetch(
        userCanister
    );
    let _canisterId = Principal.fromActor(_canister);
    likeFetchMap.put(likeFetchCanisterIndex, _canisterId);
    likeFetchCanisterIndex += 1;

    // initUserToFeed
    assert(not Principal.isAnonymous(rootFeedCanister));
    let rootFeedActor: RootFeedActor = actor(Principal.toText(rootFeedCanister));
    assert(await _canister.initUserToFeed((await rootFeedActor.getAllUserFeedCanister())));

    _canisterId
};

最后我们写出查询函数,查询各种 Fetch Canister 。

public query func getAllPostFetchCanister(): async [Principal] {
    // 获取Map中所有 Canister id
    Iter.toArray(postFetchMap.vals())
    // Iter.toArray是一个将迭代器转换为数组的函数,它将Map中所有的 Canister id 转换成一个数组
};

public query func getAllCommentFetchCanister(): async [Principal] {
    Iter.toArray(commentFetchMap.vals())
};

public query func getAllLikeFetchCanister(): async [Principal] {
    Iter.toArray(likeFetchMap.vals())
};

postFetch.mo

Post Fetch 负责把用户的帖子通知给他的粉丝。

记录哪些帖子需要通知给哪些用户,依次通知他们。用 TrieMap 快速查找和更新数据,用 Timer 来周期性执行任务。

import Types "./types";
import TrieMap "mo:base/TrieMap";
import Principal "mo:base/Principal";
import Array "mo:base/Array";
import Timer "mo:base/Timer";
import Debug "mo:base/Debug";
import Iter "mo:base/Iter";

actor class PostFetch() = this {

    // 内部维护一个通知表:记录每个用户待通知的帖子ID有哪些
    // 定义notifyMapEntries元组数组,每个元组包含一个Principal和一个文本数组[Text]
    // stable关键字确保了即使在合约升级的情况下,这个变量的数据也会被保存下来
    stable var notifyMapEntries: [(Principal, [Text])] = [];
    let notifyMap = TrieMap.fromEntries<Principal, [Text]>(notifyMapEntries.vals(), Principal.equal, Principal.hash);

    // 接收通知函数:帖子ID、发帖人、转发人、followers、Cycles
    // 当receiveNotify被调用时,它接收一个Principal数组和一个帖子ID
    public shared({caller}) func receiveNotify(to: [Principal], postId: Text): async () {
        for(_user in to.vals()) {
            Debug.print(
                "Canister PostFetch, Func receiveNotify, "
                # "to : " # Principal.toText(_user) # " ,"
                # "postId : " # postId
            );
        };
        // 每个Principal代表一个用户,函数遍历数组中的每个用户,并检查notifyMap中有没有用户的记录
        for(_follower in to.vals()) {
            switch(notifyMap.get(_follower)) {
                case(null) {
                    // 如果没有,就新建一个,只包含当前帖子ID
                    notifyMap.put(_follower, [postId]);
                };
                // 如果有,就将当前的帖子ID追加到这个用户的帖子ID数组中
                case(?_postIdArray) {
                    notifyMap.put(_follower, Array.append(_postIdArray, [postId]));
                };
            };
        };
    };

    // 提供一种查询notifyMap当前状态的方式,将TrieMap的entries转换为数组形式并返回
    public query func getNotifyMapEntries(): async [(Principal, [Text])] {
        Iter.toArray(notifyMap.entries())
    };

// userToFeed

    // 用户关系的管理
    // 定义了一个新TrieMap,保存每个用户的 principal id 和他们的 Feed Canister id 之间的关系
    stable var userToFeedEntries: [(Principal, Principal)] = [];
    var userToFeed = TrieMap.fromEntries<Principal, Principal>(userToFeedEntries.vals(), Principal.equal, Principal.hash);
    
    // 初始化userToFeed
    public shared({caller}) func initUserToFeed(_userToFeedArray: [(Principal, Principal)]): async Bool {
        userToFeed := TrieMap.fromEntries(
            _userToFeedArray.vals(),
            Principal.equal,
            Principal.hash
        );
        true
    };

    // 添加新关系
    public shared({caller}) func addUserToFeedEntry(entry: (Principal, Principal)): async Bool {
        switch(userToFeed.get(entry.0)) {
            case(?_feedCanister) { return false; };
            case(null) {
                userToFeed.put(entry.0, entry.1);
                true
            } 
        }
    };

    // 查询现有的哈希表
    public query func getUserToFeedEntries(): async [(Principal, Principal)] {
        Iter.toArray(userToFeed.entries())
    };

    public query({caller}) func whoami(): async Principal { caller };

// Timer

    type FeedActor = Types.FeedActor;

    // 根据算法用 ignore call 分批次通知followers的Feed
    // 定时器触发的回调函数
    func notify(): async () {
        // Debug.print("postFetch notify !");
        let _notifyMap = notifyMap;
        // 检查notifyMap中的每个条目,对于每个用户
        for((_user, _postIdArray) in _notifyMap.entries()) {
            // 尝试从userToFeed中获取对应的 Feed canister
            switch(userToFeed.get(_user)) {
                case(null) { };
                // 如果找到了 Feed canister,就会创建通知
                // 并调用batchReceiveFeed函数来传递所有待通知的帖子ID
                case(?_feedId) {
                    // Debug.print("Notify feed canister " # Principal.toText(_feedId));
                    let feedActor: FeedActor = actor(Principal.toText(_feedId));
                    ignore feedActor.batchReceiveFeed(_postIdArray);
                    // 从notifyMap中删除该用户的条目,以避免重复通知
                    notifyMap.delete(_user);
                };
            };
        };
    };

    // 定义一个定时器cycleTimer,它定期(每2秒)调用notify函数
    // notify函数的目的是通知用户他们的Feed有更新
    let cycleTimer = Timer.recurringTimer(
        #seconds(2),
        notify
    );

    // 系统函数,前面说过,这里就不赘述了
    system func preupgrade() {
        notifyMapEntries := Iter.toArray(notifyMap.entries());
        userToFeedEntries := Iter.toArray(userToFeed.entries());
    };

    system func postupgrade() {
        notifyMapEntries := [];
        userToFeedEntries := [];
    };
};

likeFetch.mo

Like Fetch Canister 和 Post Fetch Canister 很像,只有一个流程上的区别。Like Fetch Canister 会根据帖子的主人查询粉丝,然后通知粉丝们,而 Post Fetch Canister 只负责根据发帖人直接发的粉丝名单通知。

import Principal "mo:base/Principal";
import TrieMap "mo:base/TrieMap";
import Types "./types";
import Array "mo:base/Array";
import Timer "mo:base/Timer";
import Iter "mo:base/Iter";
import Debug "mo:base/Debug";

actor class LikeFetch(
    userCanister: Principal
) = this {

    // 定义类型别名
    type UserActor = Types.UserActor;
    type PostImmutable = Types.PostImmutable;
    type Repost = Types.Repost;

    // 使用一个稳定的变量notifyMapEntries来存储通知(Principal和一组Text的元组)
    // 初始化一个TrieMap作为实际的通知Map
    stable var notifyMapEntries: [(Principal, [Text])] = [];
    let notifyMap = TrieMap.fromEntries<Principal, [Text]>(notifyMapEntries.vals(), Principal.equal, Principal.hash);
    
    // 接收帖子的通知
    // 首先从用户的actor里获取帖子主人的粉丝列表,然后调用_storeNotify函数来为粉丝和转发者存储通知
    public shared({caller}) func receiveNotify(post: PostImmutable): async () {
        // 查到这个帖子的主用户的followers
        let userActor: UserActor = actor(Principal.toText(userCanister));
        let postUserFollowers = await userActor.getFollowersList(post.user);

        // 通知粉丝
        _storeNotify(postUserFollowers, post.postId);

        // 通知转发帖子的人
        _storeNotify(
            Array.map<Repost, Principal>(
                post.repost,
                func (x: Repost): Principal {
                    x.user
                }
            ), 
            post.postId
        );
    };

    public shared({caller}) func receiveRepostUserNotify(to: [Principal], postId: Text): async () {
        _storeNotify(to, postId);
    };

    public query func getNotifyMapEntries(): async [(Principal, [Text])] {
        Iter.toArray(notifyMap.entries())
    };

    // 存储通知
    // 它检查指定的Principal是否已经有通知列表
    // 如果没有则创建一个新的列表并添加帖子ID,如果有则在现有的列表中添加帖子ID
    private func _storeNotify(to: [Principal], postId: Text) {
        for(_follower in to.vals()) {
            switch(notifyMap.get(_follower)) {
                case(null) {
                    notifyMap.put(_follower, [postId]);
                };
                case(?_postIdArray) {
                    let _newPostIdArray = Array.append(_postIdArray, [postId]);
                    notifyMap.put(_follower, _newPostIdArray);
                };
            };
        };
    };
// userToFeed

    stable var userToFeedEntries: [(Principal, Principal)] = [];
    var userToFeed = TrieMap.fromEntries<Principal, Principal>(userToFeedEntries.vals(), Principal.equal, Principal.hash);

    public shared({caller}) func initUserToFeed(_userToFeedArray: [(Principal, Principal)]): async Bool {
        userToFeed := TrieMap.fromEntries(
            _userToFeedArray.vals(),
            Principal.equal,
            Principal.hash
        );
        true
    };

    public shared({caller}) func addUserToFeedEntry(entry: (Principal, Principal)): async Bool {
        switch(userToFeed.get(entry.0)) {
            case(?_feedCanister) { return false; };
            case(null) {
                userToFeed.put(entry.0, entry.1);
                true
            } 
        }
    };

    public query func getUserToFeedEntries(): async [(Principal, Principal)] {
        Iter.toArray(userToFeed.entries())
    };
    
    public query({caller}) func whoami(): async Principal { caller };

// Timer

    type FeedActor = Types.FeedActor;

    func notify(): async () {
        // Debug.print("likeFetch notify !");
        let _notifyMap = notifyMap; 
        for((_user, _postIdArray) in _notifyMap.entries()) {
            switch(userToFeed.get(_user)) {
                case(null) { };
                case(?_feedId) {
                    let feedActor: FeedActor = actor(Principal.toText(_feedId));
                    ignore feedActor.batchReceiveLike(_postIdArray);
                    notifyMap.delete(_user);
                };
            };
        };
    };

    let cycleTimer = Timer.recurringTimer(
        #seconds(2),
        notify
    );

    system func preupgrade() {
        notifyMapEntries := Iter.toArray(notifyMap.entries());
        userToFeedEntries := Iter.toArray(userToFeed.entries());
    };

    system func postupgrade() {
        notifyMapEntries := [];
        userToFeedEntries := [];
    };

};

commentFetch.mo

Comment Fetch Canister 和 Like Fetch Canister 是一样的,所以就不重复讲了。

以下是 commentFetch.mo 源文件:

import Principal "mo:base/Principal";
import TrieMap "mo:base/TrieMap";
import Types "./types";
import Array "mo:base/Array";
import Timer "mo:base/Timer";
import Iter "mo:base/Iter";
import Debug "mo:base/Debug";

actor class CommentFetch(
    userCanister: Principal
) = this {
    
    type UserActor = Types.UserActor;
    type PostImmutable = Types.PostImmutable;
    type Repost = Types.Repost;

    // 一个列表,其中每个元素都是一个元组,包含一个Principal和一个文本字符串的数组
    // 这个列表被用来初始化notifyMap,后者是一个TrieMap结构,用来高效地存储和检索键值对
    stable var notifyMapEntries: [(Principal, [Text])] = [];
    let notifyMap = TrieMap.fromEntries<Principal, [Text]>(notifyMapEntries.vals(), Principal.equal, Principal.hash);
    
    public shared({caller}) func receiveNotify(post: PostImmutable): async () {
        // 查到这个帖子的主用户的 followers
        let userActor: UserActor = actor(Principal.toText(userCanister));
        let postUserFollowers = await userActor.getFollowersList(post.user);

        // 通知粉丝
        _storeNotify(postUserFollowers, post.postId);

        // 通知转帖者
        _storeNotify(
            Array.map<Repost, Principal>(
                post.repost,
                func (x: Repost): Principal {
                    x.user
                }
            ), 
            post.postId
        );
    };

    public shared({caller}) func receiveRepostUserNotify(to: [Principal], postId: Text): async () {
        _storeNotify(to, postId);
    };

    private func _storeNotify(to: [Principal], postId: Text) {
        for(_follower in to.vals()) {
            switch(notifyMap.get(_follower)) {
                case(null) {
                    notifyMap.put(_follower, [postId]);
                };
                case(?_postIdArray) {
                    notifyMap.put(_follower, Array.append(_postIdArray, [postId]));
                };
            };
        };
    };

    public query func getNotifyMapEntries(): async [(Principal, [Text])] {
        Iter.toArray(notifyMap.entries())
    };

// userToFeed

    stable var userToFeedEntries: [(Principal, Principal)] = [];
    var userToFeed = TrieMap.fromEntries<Principal, Principal>(userToFeedEntries.vals(), Principal.equal, Principal.hash);

    public shared({caller}) func initUserToFeed(_userToFeedArray: [(Principal, Principal)]): async Bool {
        userToFeed := TrieMap.fromEntries(
            _userToFeedArray.vals(),
            Principal.equal,
            Principal.hash
        );
        true
    };

    public shared({caller}) func addUserToFeedEntry(entry: (Principal, Principal)): async Bool {
        switch(userToFeed.get(entry.0)) {
            case(?_feedCanister) { return false; };
            case(null) {
                userToFeed.put(entry.0, entry.1);
                true
            } 
        }
    };

    public query func getUserToFeedEntries(): async [(Principal, Principal)] {
        Iter.toArray(userToFeed.entries())
    };
    
    public query({caller}) func whoami(): async Principal { caller };

// Timer

    type FeedActor = Types.FeedActor;

    func notify(): async () {
        // Debug.print("commentFetch notify !");
        let _notifyMap = notifyMap; 
        for((_user, _postIdArray) in _notifyMap.entries()) {
            switch(userToFeed.get(_user)) {
                case(null) { };
                case(?_feedId) {
                    // Debug.print("commentFetch Notify feed canister " # Principal.toText(_feedId));
                    let feedActor: FeedActor = actor(Principal.toText(_feedId));
                    ignore feedActor.batchReceiveComment(_postIdArray);
                    notifyMap.delete(_user);
                };
            };
        };
    };

    let cycleTimer = Timer.recurringTimer(
        #seconds(2),
        notify
    );

    system func preupgrade() {
        notifyMapEntries := Iter.toArray(notifyMap.entries());
        userToFeedEntries := Iter.toArray(userToFeed.entries());
    };

    system func postupgrade() {
        notifyMapEntries := [];
        userToFeedEntries := [];
    };
};