From ad1f6390e8d4b9992fd0ef5e1ea897c5f0d5101e Mon Sep 17 00:00:00 2001 From: 552020 Date: Wed, 19 Feb 2025 16:41:58 +0100 Subject: [PATCH 1/7] chore: create copy of pub-sub and update README with objective of this reloaded version --- motoko/pub-sub-reloaded/Makefile | 38 +++++++++++++ motoko/pub-sub-reloaded/README.md | 73 +++++++++++++++++++++++++ motoko/pub-sub-reloaded/dfx.json | 12 ++++ motoko/pub-sub-reloaded/src/pub/Main.mo | 29 ++++++++++ motoko/pub-sub-reloaded/src/sub/Main.mo | 28 ++++++++++ 5 files changed, 180 insertions(+) create mode 100644 motoko/pub-sub-reloaded/Makefile create mode 100644 motoko/pub-sub-reloaded/README.md create mode 100644 motoko/pub-sub-reloaded/dfx.json create mode 100644 motoko/pub-sub-reloaded/src/pub/Main.mo create mode 100644 motoko/pub-sub-reloaded/src/sub/Main.mo diff --git a/motoko/pub-sub-reloaded/Makefile b/motoko/pub-sub-reloaded/Makefile new file mode 100644 index 000000000..9517ae0cb --- /dev/null +++ b/motoko/pub-sub-reloaded/Makefile @@ -0,0 +1,38 @@ +.PHONY: all +all: build + +.PHONY: build +.SILENT: build +build: + dfx canister create --all + dfx build + +.PHONY: install +.SILENT: install +install: build + dfx canister install --all + +.PHONY: upgrade +.SILENT: upgrade +upgrade: build + dfx canister install --all --mode=upgrade + +.PHONY: test +.SILENT: test +test: install + dfx canister call sub init '("Apples")' + dfx canister call sub getCount \ + | grep '(0 : nat)' && echo 'PASS' + dfx canister call pub publish '(record { "topic" = "Apples"; "value" = 2 })' + sleep 2 # Wait for update. + dfx canister call sub getCount \ + | grep '(2 : nat)' && echo 'PASS' + dfx canister call pub publish '(record { "topic" = "Bananas"; "value" = 3 })' + sleep 2 # Wait for update. + dfx canister call sub getCount \ + | grep '(2 : nat)' && echo 'PASS' + +.PHONY: clean +.SILENT: clean +clean: + rm -fr .dfx diff --git a/motoko/pub-sub-reloaded/README.md b/motoko/pub-sub-reloaded/README.md new file mode 100644 index 000000000..da04d3826 --- /dev/null +++ b/motoko/pub-sub-reloaded/README.md @@ -0,0 +1,73 @@ +# PubSub Reloaded + +This project enhances the original [PubSub example](link-to-original) to provide a clearer demonstration of inter-canister calls on the Internet Computer, specifically showing how functions can be passed as arguments between canisters. While maintaining the simplicity of the original design, this version improves the architecture by: + +1. Clearly defining the three key roles in a pub/sub system: + + - Publisher: manages subscriptions and broadcasts messages + - Subscribers: receive and process messages for their topics of interest + - Content Creator: generates the content to be published (previously implicit in the original design) + +2. Implementing a more intuitive message type: replacing the `Counter` type with a `NewsMessage` type that better represents a real-world pub/sub scenario + +3. Supporting multiple subscribers out of the box, with a pre-configured setup that demonstrates how multiple subscribers can receive updates for the same topics + +The example try to maintain the original's simplicity while providing a more practical and comprehensive demonstration of pub/sub principles. + +## Prerequisites + +This example requires an installation of: + +- [x] Install the [IC SDK](https://internetcomputer.org/docs/current/developer-docs/setup/install/index.mdx). +- [x] Clone the example dapp project: `git clone https://github.com/dfinity/examples` + +Begin by opening a terminal window. + +## Step 1: Setup the project environment + +Navigate into the folder containing the project's files and start a local instance of the Internet Computer with the commands: + +```bash +cd examples/motoko/pub-sub +dfx start --background +``` + +## Step 2: Deploy the canisters: + +```bash +dfx deploy +``` + +## Step 3: Subscribe to the "Apples" topic + +```bash +dfx canister call sub init '("Apples")' +``` + +## Step 4: Publish to the "Apples" topic + +```bash +dfx canister call pub publish '(record { "topic" = "Apples"; "value" = 2 })' +``` + +## Step 5: Receive your subscription + +```bash +dfx canister call sub getCount +``` + +The output should resemble the following: + +```bash +(2 : nat) +``` + +## Security considerations and best practices + +If you base your application on this example, we recommend you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/current/references/security/) for developing on the Internet Computer. This example may not implement all the best practices. + +For example, the following aspects are particularly relevant for this app, since it makes inter-canister calls: + +- [Be aware that state may change during inter-canister calls.](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview) +- [Only make inter-canister calls to trustworthy canisters.](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview) +- [Don't panic after await and don't lock shared resources across await boundaries.](https://internetcomputer.org/docs/current/developer-docs/security/security-best-practices/overview) diff --git a/motoko/pub-sub-reloaded/dfx.json b/motoko/pub-sub-reloaded/dfx.json new file mode 100644 index 000000000..0a5d9a3e0 --- /dev/null +++ b/motoko/pub-sub-reloaded/dfx.json @@ -0,0 +1,12 @@ +{ + "canisters": { + "pub": { + "type": "motoko", + "main": "src/pub/Main.mo" + }, + "sub": { + "type": "motoko", + "main": "src/sub/Main.mo" + } + } +} \ No newline at end of file diff --git a/motoko/pub-sub-reloaded/src/pub/Main.mo b/motoko/pub-sub-reloaded/src/pub/Main.mo new file mode 100644 index 000000000..5df666704 --- /dev/null +++ b/motoko/pub-sub-reloaded/src/pub/Main.mo @@ -0,0 +1,29 @@ +// Publisher +import List "mo:base/List"; + +actor Publisher { + + type Counter = { + topic : Text; + value : Nat; + }; + + type Subscriber = { + topic : Text; + callback : shared Counter -> (); + }; + + stable var subscribers = List.nil(); + + public func subscribe(subscriber : Subscriber) { + subscribers := List.push(subscriber, subscribers); + }; + + public func publish(counter : Counter) { + for (subscriber in List.toArray(subscribers).vals()) { + if (subscriber.topic == counter.topic) { + subscriber.callback(counter); + }; + }; + }; +} diff --git a/motoko/pub-sub-reloaded/src/sub/Main.mo b/motoko/pub-sub-reloaded/src/sub/Main.mo new file mode 100644 index 000000000..1588c71bc --- /dev/null +++ b/motoko/pub-sub-reloaded/src/sub/Main.mo @@ -0,0 +1,28 @@ +// Subscriber + +import Publisher "canister:pub"; + +actor Subscriber { + + type Counter = { + topic : Text; + value : Nat; + }; + + var count: Nat = 0; + + public func init(topic0 : Text) { + Publisher.subscribe({ + topic = topic0; + callback = updateCount; + }); + }; + + public func updateCount(counter : Counter) { + count += counter.value; + }; + + public query func getCount() : async Nat { + count; + }; +} From 0bb931878c4b97362c713c4b9cc4cf352c4f672b Mon Sep 17 00:00:00 2001 From: 552020 Date: Wed, 19 Feb 2025 16:44:58 +0100 Subject: [PATCH 2/7] feat: add multiple subscriber canisters to dfx.json --- motoko/pub-sub-reloaded/dfx.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/motoko/pub-sub-reloaded/dfx.json b/motoko/pub-sub-reloaded/dfx.json index 0a5d9a3e0..9fc4d570d 100644 --- a/motoko/pub-sub-reloaded/dfx.json +++ b/motoko/pub-sub-reloaded/dfx.json @@ -4,9 +4,17 @@ "type": "motoko", "main": "src/pub/Main.mo" }, - "sub": { + "sub1": { + "type": "motoko", + "main": "src/sub/Main.mo" + }, + "sub2": { + "type": "motoko", + "main": "src/sub/Main.mo" + }, + "sub3": { "type": "motoko", "main": "src/sub/Main.mo" } } -} \ No newline at end of file +} From ba72023ee229c3b1ab36704cc1212287db7f8b34 Mon Sep 17 00:00:00 2001 From: 552020 Date: Wed, 19 Feb 2025 18:16:25 +0100 Subject: [PATCH 3/7] docs: update README with analysis of PubSub and new plan --- motoko/pub-sub-reloaded/README.md | 93 ++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/motoko/pub-sub-reloaded/README.md b/motoko/pub-sub-reloaded/README.md index da04d3826..61f5d9fe3 100644 --- a/motoko/pub-sub-reloaded/README.md +++ b/motoko/pub-sub-reloaded/README.md @@ -12,7 +12,98 @@ This project enhances the original [PubSub example](link-to-original) to provide 3. Supporting multiple subscribers out of the box, with a pre-configured setup that demonstrates how multiple subscribers can receive updates for the same topics -The example try to maintain the original's simplicity while providing a more practical and comprehensive demonstration of pub/sub principles. +The example maintains the original's simplicity while providing a more practical and comprehensive demonstration of inter-canister communication. + +## Overview and Architecture of the Original PubSub App + +The original PubSub example implements a system similar to a mailing list or feed subscription service. + +### Subscription + +Subscribers can register their interest in specific topics through their public `init` function. + +```motoko + public func init(topic0 : Text) { + Publisher.subscribe({ + topic = topic0; + callback = updateCount; + }); + }; +``` + +Note that: + +- `init` takes a topic as an argument, which is of type Text. The topic can be whatever topic: the subscriber is not just subscribing topics made available by the publisher, but any possible topic. + +- `init` triggers an inter-canister call to the Publisher's `subscribe` function, passing, the topic they're interested in and a callback function (`updateCount`) that will be invoked when new messages arrive. This inter-canister communication is made possible by the Subscriber importing the Publisher canister: `import Publisher "canister:pub"` and the callback function being `public` in the Subscriber actor - Motoko automatically treats public functions from actors as shared when used as inter-canister calls. + +The publisher's subscribe function definition and the definition of the type Subscriber and the list subscribers are the following: + +```motoko +type Subscriber = { + topic : Text; + callback : shared Counter -> (); +}; +stable var subscribers = List.nil(); + + public func subscribe(subscriber : Subscriber) { + subscribers := List.push(subscriber, subscribers); + }; +``` + +As we can see from the definitions, when the subscribe function is called, an instance of the Subscriber type is added to the list of subscribers. + +Note that: + +1. The `subscribers` list doesn't track unique subscribers, but rather subscription entries. Each call to `init` adds a new entry to the list, regardless of whether the calling canister has already subscribed to the same or different topics. This means a single subscriber canister can appear multiple times in the list with different topic subscriptions. + +2. Every subscriber passes the same function 'updateCount' as the callback function required in the Subscriber type. The different canisters are identified through the fact that the reference of the passed function is different. + +3. The `shared` keyword in Motoko is used to designate functions that can be called across canisters. While public actor methods are implicitly shared, the type system needs explicit `shared` annotations when describing function types that will be used for inter-canister calls. For a detailed explanation of sharing functions between actors, see the [Motoko documentation on sharing](https://internetcomputer.org/docs/current/motoko/main/writing-motoko/sharing#the-shared-keyword). + +### Content creation and publishing (broadcasting) + +If we imagine the PubSub model as a mailing list or a blog, normally we have some content creators and subscribers of the content. The PubSub app resembles the model of a mailing list, where anyone can send a message. The message of the original PubSub app was of type Counter: + +```motoko +type Counter = { + topic : Text; + value : Nat; +}; +``` + +Each subscriber maintains a counter variable and an update function: + +```motoko +var count: Nat = 0; +public func updateCount(counter : Counter) { + count += counter.value; +}; +``` + +For example, the topic could be "Astronauts" and the value "5". Every time a message of type Counter is published, if the subscriber has subscribed to that message's topic, its internal count variable is increased by the amount specified in the value. + +So if a subscriber subscribes to "Astronauts", and then a Counter message is published with an "Astronauts" topic and a value of 5, and then another message with topic of "Astronauts" is published with value 3, the internal counter of the subscriber will be 8. Note that if a subscriber subscribes to multiple topics, the counter will maintain a unique sum for all of them. + +## Proposed Enhancements + +To make this small application more realistic, we will change the type of the broadcasted message to NewsMessage: + +```motoko +type NewsMessage = { + topic : Text; + content : Text; + readingTime : Nat; +}; +``` + +This change makes the example more intuitive by: + +- Keeping the topic-based subscription mechanism +- Adding actual content (Text) that represents the news message +- Replacing the arbitrary `value` field with a meaningful `readingTime` field that represents the estimated time to read the message + +The `readingTime` field maintains the original example's counter functionality (subscribers can track total reading time for their topics) while making the application represent a more realistic news broadcasting scenario. ## Prerequisites From 7f1ec24b75180a5cb0e575e3800d95f0f2b02bec Mon Sep 17 00:00:00 2001 From: 552020 Date: Wed, 19 Feb 2025 18:54:25 +0100 Subject: [PATCH 4/7] Docs: explain all the modification --- motoko/pub-sub-reloaded/README.md | 85 ++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/motoko/pub-sub-reloaded/README.md b/motoko/pub-sub-reloaded/README.md index 61f5d9fe3..eb2bbde60 100644 --- a/motoko/pub-sub-reloaded/README.md +++ b/motoko/pub-sub-reloaded/README.md @@ -85,7 +85,7 @@ For example, the topic could be "Astronauts" and the value "5". Every time a mes So if a subscriber subscribes to "Astronauts", and then a Counter message is published with an "Astronauts" topic and a value of 5, and then another message with topic of "Astronauts" is published with value 3, the internal counter of the subscriber will be 8. Note that if a subscriber subscribes to multiple topics, the counter will maintain a unique sum for all of them. -## Proposed Enhancements +## Enhancements To make this small application more realistic, we will change the type of the broadcasted message to NewsMessage: @@ -105,6 +105,89 @@ This change makes the example more intuitive by: The `readingTime` field maintains the original example's counter functionality (subscribers can track total reading time for their topics) while making the application represent a more realistic news broadcasting scenario. +Therefore, the `count` state of the subscriber has been changed to `totalReadingTime`, which represents the time subscribers would have spent if they had read all the messages they subscribed to. In this context, it makes sense to have an increasing counter even if the subscriber subscribes to multiple topics, as it tracks total reading time across all subscriptions. + +The function `init` has been renamed to `subscribeToTopic` as it better reflects its purpose - it's not really initializing anything and can be called multiple times. The new name makes the function's behavior more explicit and self-documenting. + +Similarly, `updateCount` becomes `updateTotalReadingTime` to align with the new message type and state variable. This function now adds the reading time of each new message to the subscriber's total, providing a meaningful metric of content consumption. + +Finally, the query function `getCount` is renamed to `getTotalReadingTime` to maintain consistency with the new terminology and provide a clearer indication of what information it returns. + +### Summary of Changes + +1. Message Type: + +```motoko +// OLD +type Counter = { + topic : Text; + value : Nat; +}; + +// NEW +type NewsMessage = { + topic : Text; + content : Text; + readingTime : Nat; +}; +``` + +2. Subscriber State: + +```motoko +// OLD +var count: Nat = 0; + +// NEW +var totalReadingTime: Nat = 0; +``` + +3. Subscriber Functions: + +```motoko +// OLD +public func init(topic0 : Text) + +// NEW +public func subscribeToTopic(subscribedTopic : Text) +``` + +```motoko +// OLD +public func updateCount(counter : Counter) { + count += counter.value; +}; + +// NEW +public func updateTotalReadingTime(message : NewsMessage) { + totalReadingTime += message.readingTime; +}; +``` + +```motoko +// OLD +public query func getCount() : async Nat + +// NEW +public query func getTotalReadingTime() : async Nat +``` + +4. Publisher Type: + +```motoko +// OLD +type Subscriber = { + topic : Text; + callback : shared Counter -> (); +}; + +// NEW +type Subscriber = { + topic : Text; + callback : shared NewsMessage -> (); +}; +``` + ## Prerequisites This example requires an installation of: From ad4592cdf59a1a838dd352aade5f608ecc64963d Mon Sep 17 00:00:00 2001 From: 552020 Date: Wed, 19 Feb 2025 18:56:10 +0100 Subject: [PATCH 5/7] feat(canisters): implement NewsMessage type and update subscriber logic - Replace Counter type with NewsMessage type for better real-world representation - Rename subscriber functions for clarity (init -> subscribeToTopic) - Update state tracking from count to totalReadingTime - Adjust callback functions to handle NewsMessage type " --- motoko/pub-sub-reloaded/src/pub/Main.mo | 13 +++++++------ motoko/pub-sub-reloaded/src/sub/Main.mo | 23 +++++++++++++---------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/motoko/pub-sub-reloaded/src/pub/Main.mo b/motoko/pub-sub-reloaded/src/pub/Main.mo index 5df666704..3191bbf3a 100644 --- a/motoko/pub-sub-reloaded/src/pub/Main.mo +++ b/motoko/pub-sub-reloaded/src/pub/Main.mo @@ -3,14 +3,15 @@ import List "mo:base/List"; actor Publisher { - type Counter = { + type NewsMessage = { topic : Text; - value : Nat; + content : Text; + readingTime : Nat; }; type Subscriber = { topic : Text; - callback : shared Counter -> (); + callback : shared NewsMessage -> (); }; stable var subscribers = List.nil(); @@ -19,10 +20,10 @@ actor Publisher { subscribers := List.push(subscriber, subscribers); }; - public func publish(counter : Counter) { + public func publish(newsMessage : NewsMessage) { for (subscriber in List.toArray(subscribers).vals()) { - if (subscriber.topic == counter.topic) { - subscriber.callback(counter); + if (subscriber.topic == newsMessage.topic) { + subscriber.callback(newsMessage); }; }; }; diff --git a/motoko/pub-sub-reloaded/src/sub/Main.mo b/motoko/pub-sub-reloaded/src/sub/Main.mo index 1588c71bc..d4413b96e 100644 --- a/motoko/pub-sub-reloaded/src/sub/Main.mo +++ b/motoko/pub-sub-reloaded/src/sub/Main.mo @@ -4,25 +4,28 @@ import Publisher "canister:pub"; actor Subscriber { - type Counter = { + +type NewsMessage = { topic : Text; - value : Nat; + content : Text; + readingTime : Nat; }; - var count: Nat = 0; + var totalReadingTime: Nat = 0; - public func init(topic0 : Text) { + public func subscribeToTopic(subscribedTopic : Text) { Publisher.subscribe({ - topic = topic0; - callback = updateCount; + topic = subscribedTopic; + callback = updateTotalReadingTime; }); }; - public func updateCount(counter : Counter) { - count += counter.value; + public func updateCount(newsMessage : NewsMessage) { + totalReadingTime += newsMessage.readingTime; }; - public query func getCount() : async Nat { - count; + public query func getTotalReadingTime() : async Nat { + totalReadingTime; }; } + From 701c109d6859f937a1c97e107d8e0a58f66bff3c Mon Sep 17 00:00:00 2001 From: 552020 Date: Wed, 19 Feb 2025 19:11:57 +0100 Subject: [PATCH 6/7] fix: missing renaming --- motoko/pub-sub-reloaded/src/sub/Main.mo | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/motoko/pub-sub-reloaded/src/sub/Main.mo b/motoko/pub-sub-reloaded/src/sub/Main.mo index d4413b96e..a3613f253 100644 --- a/motoko/pub-sub-reloaded/src/sub/Main.mo +++ b/motoko/pub-sub-reloaded/src/sub/Main.mo @@ -20,8 +20,8 @@ type NewsMessage = { }); }; - public func updateCount(newsMessage : NewsMessage) { - totalReadingTime += newsMessage.readingTime; + public func updateTotalReadingTime(message : NewsMessage) { + totalReadingTime += message.readingTime; }; public query func getTotalReadingTime() : async Nat { From 738aed38cf2c0224bd34c7b2555ba749b1013019 Mon Sep 17 00:00:00 2001 From: 552020 Date: Wed, 19 Feb 2025 19:23:50 +0100 Subject: [PATCH 7/7] docs: improve example walkthrough with multi-subscriber scenario --- motoko/pub-sub-reloaded/README.md | 86 ++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/motoko/pub-sub-reloaded/README.md b/motoko/pub-sub-reloaded/README.md index eb2bbde60..2d65c51a9 100644 --- a/motoko/pub-sub-reloaded/README.md +++ b/motoko/pub-sub-reloaded/README.md @@ -197,6 +197,14 @@ This example requires an installation of: Begin by opening a terminal window. +In this example, we'll demonstrate how the pub/sub system works with three subscribers: + +- sub1: Will subscribe to both "Astronauts" and "Aliens" topics +- sub2: Will subscribe only to "Astronauts" topic +- sub3: Will subscribe only to "Aliens" topic + +This setup will show how subscribers can handle multiple topics and how different subscribers can receive updates for the same topic. + ## Step 1: Setup the project environment Navigate into the folder containing the project's files and start a local instance of the Internet Computer with the commands: @@ -212,30 +220,92 @@ dfx start --background dfx deploy ``` -## Step 3: Subscribe to the "Apples" topic +## Step 3: Subscribe to the "Astronauts" topic + +```bash +dfx canister call sub1 subscribeToTopic '("Astronauts")' +``` + +## Step 4: Publish news about the Moon landing ```bash -dfx canister call sub init '("Apples")' +dfx canister call pub publish '(record { + "topic" = "Astronauts"; + "content" = "Historic moment: Humans first landed on the Moon!"; + "readingTime" = 3 +})' ``` -## Step 4: Publish to the "Apples" topic +## Step 5: Check sub1's reading time ```bash -dfx canister call pub publish '(record { "topic" = "Apples"; "value" = 2 })' +dfx canister call sub1 getTotalReadingTime ``` -## Step 5: Receive your subscription +The output should be `(3 : nat)`, indicating 3 time units spent reading about the Moon landing. + +## Step 6: Add another subscriber to Astronauts ```bash -dfx canister call sub getCount +dfx canister call sub2 subscribeToTopic '("Astronauts")' ``` -The output should resemble the following: +## Step 7: Publish Mars mission news ```bash -(2 : nat) +dfx canister call pub publish '(record { + "topic" = "Astronauts"; + "content" = "Elon Musk announces plans for first human Mars landing"; + "readingTime" = 5 +})' ``` +## Step 8: Check both subscribers' reading times + +```bash +dfx canister call sub1 getTotalReadingTime +dfx canister call sub2 getTotalReadingTime +``` + +Sub1 should show `(8 : nat)` (Moon + Mars news), while sub2 shows `(5 : nat)` (only Mars news). + +## Step 9: Subscribe to Aliens news + +```bash +dfx canister call sub1 subscribeToTopic '("Aliens")' +dfx canister call sub3 subscribeToTopic '("Aliens")' +``` + +## Step 10: Publish Aliens news + +```bash +dfx canister call pub publish '(record { + "topic" = "Aliens"; + "content" = "Today aliens have visited the Earth. They are green as expected. They came in peace"; + "readingTime" = 4 +})' +``` + +## Step 11: Final reading time check + +```bash +dfx canister call sub1 getTotalReadingTime +dfx canister call sub2 getTotalReadingTime +dfx canister call sub3 getTotalReadingTime +``` + +You should see: + +- sub1: `(12 : nat)` (Moon + Mars + Aliens news) +- sub2: `(5 : nat)` (only Mars news) +- sub3: `(4 : nat)` (only Aliens news) + +This demonstrates how: + +1. Subscribers can subscribe to multiple topics (sub1) +2. Multiple subscribers can subscribe to the same topic (sub1 and sub2 for Astronauts) +3. Reading times accumulate across all subscribed topics + ## Security considerations and best practices If you base your application on this example, we recommend you familiarize yourself with and adhere to the [security best practices](https://internetcomputer.org/docs/current/references/security/) for developing on the Internet Computer. This example may not implement all the best practices.