Newbie - help with seeing resclient.js change messages

#1

Hi,

I’ve taken the GitHub - resgateio/resclient: Javascript client implementing the RES-Client Protocol. ‘Example Usage’ js code and changed it to look at my locally running ‘library.books’ restgate service, below.

var ResClient = resclient.default;

    const client = new ResClient('ws://localhost:8001');

    client.get('library.books').then(books => {
        console.log(books);

        let onChange = () => {
            console.log("Books: " + books);
        };

        // Listen to changes 'eventually' unsubscribing
        books.on('change', onChange);
        setTimeout(() => {
            books.off('change', onChange);
        }, 500000000);
    });

I’m able to open the Vue.js example and see changes in different browser instances running the same Vue.js code but I’m not seeing any OnChange console message to the books model in the above resclient.js code?

The above code is connecting and rendering the books model in the console within the simple HTML page I’ve created

What do I need to modify to see ‘library.books’ model changes?

Many thanks in advance.

#2

Welcome to the forum! :partying_face:

You won’t get any change events on library.books because that resource is a collection, not a model. The defined events for collections are add and remove. Only the books that the collection contains (eg. library.book.1) are models.

And ResClient does not propagate events from referenced resources (meaning, listener on library.books will not trigger on events for library.book.1), but you have have to listen to them separately. You often listen to the add/remove event in the component that renders the list, while you listen to change events in the component that renders the specific model.

Listening for all events would be:

const ResClient = resclient.default;

const client = new ResClient('ws://localhost:8001');

client.get('library.books').then(books => {
  console.log(books);

  let onChange = (changed, book) => {
    console.log("Book changed: " + JSON.stringify(book));
  };
  let onAdd = ({ item, idx }) => {
    console.log("Book added at index " + idx + ": " + JSON.stringify(item));
    // Listen to change events on added book
    item.on('change', onChange);
  };
  let onRemove = ({ item, idx }) => {
    console.log("Book removed at index " + idx + ": " + JSON.stringify(item));
    // Unlisten to change event on removed book
    item.off('change', onChange);
  };

  // Listen to add/remove/change events, 'eventually' unsubscribing
  books.on('add', onAdd);
  books.on('remove', onRemove);
  // Loop through all books and listen to them separately
  for (let book of books) {
    book.on('change', onChange);
  }
  setTimeout(() => {
    books.off('add', onAdd);
    books.off('remove', onRemove);
    for (let book of books) {
      book.off('change', onChange);
    }
  }, 500000000);
});

Does that answer the question? Or do you need a central way of listening to all events, like for event logging?

Best regards,
Samuel

#3

Wow! What a prompt response!

Thank you Samuel for such a detailed and incitful answer.

This certainly answers my current question on understanding what could be achived by resgate and how I could use it for realtime, light weight and a scalable solution for a project we’re working on.

I’m also very interested in Modapp especially ModelTxt. Is it possible to use ModelTxt in the context of a pre-rendered HTML page or is this only available if Modapp creates the element? We’re looking a 4K rendered HTML pages that we only need to update a small number of the thousands of elements without having to refresh the page.

Again, many, many thanks for your detailed and complete explaination as I’m learning your great work.

#4

Glad you are trying it out! :blush:

It is possible for sure. In essence, ModelTxt is just an object with a render() method like this:

render(el) {
  // Create its own span element
  this.span = document.createElement('span');
  // Get the text by calling the provided callback with the model
  let text = this.callback(this.model);
  this.span.textContent = text;
  // Append the span to the provided DOM element
  el.appendChild(this.span);
  // Listen to changes on the element
  this.model.on('change', this.onChange);
  return this.span;
}

onChange = () => {
  // Set the text again in case it has changed.
  // In ModelTxt, this is done with a fade animation,
  // only if there was an actual change.
  let text = this.callback(this.model);
  this.span.textContent = text;
}

So, there isn’t much “magic” about ModelTxt. No real dependencies. The unrender method is also simple: it just removes the span from the DOM and calls model.off.
Writing your own slim component is quite easy as well, incase you don’t want the additional span that ModelTxt wraps the text in, or if you want something else than the fade transition.

About using it in pre-rendered HTML. Let’s say you receive:

<body>
<ul>
    <li id="item1">Initial text</li>
</ul>
</body>

Then, you can have a little javascript that “hi-jack” one of the elements:

client.get('library.book.1', book => {
  // Get the element to hi-jack
  let el = document.getElementById('item1');
  // Delete all pre-rendered content, if you want to. Not needed.
  while (el.lastChild) el.removeChild(el.lastChild);
  // Create a ModelTxt and render it inside the element.
  let component = new ModelTxt(book, book => book.title);
  component.render(el);
});

But it is good to understand that Modapp Components isn’t really a framework, just some interfaces. It was a defined way by which we decided to create vanilla Javascript components. We built components as we needed them, using the basic Elem component to avoid boilerplate (annoying to have to write document.createElement('div') all the time!).

And it has worked out amazingly well, actually. The concept of components that are capable of rendering themselves, and in the case of resource components such as ModelTxt, also updating themselves, has been really smooth!

Hope your attempts works out fine! :grinning:

Best regards,
Samuel

#5

Hi Samuel,

I’m now working on the server-side with my own models. But to keep things simple, in the context of the Books go code example, where the model is:

var bookModels = map[string]*Book{
    "library.book.1": {ID: 1, Title: "Animal Farm", Author: "George Orwell"},
    "library.book.2": {ID: 2, Title: "Brave New World", Author: "Aldous Huxley"},
    "library.book.3": {ID: 3, Title: "Coraline", Author: "Neil Gaiman"},
}

Using your library ‘github.com/jirenius/go-res’ how would one write a custom update event to indicate that ‘library.book.1’ has been modified using the RES service.With() function in particular the ‘r.ChangeEvent’ call?

Many thanks in advance.

#6

No problem. I figured it out …

s.With(“library.book.1”, func(r res.Resource) {
r.ChangeEvent(map[string]interface{}{“Title”: “New Name”, “Author”: “New Author”})
})

#7

Hi Craig,

Hope things are going well!

The book-collection example shows how to send change events from within a Set handler. But you can use With in case the update comes from some other “external” event.

Let’s say we should also be able to update it from the terminal as well.
To do that, we can add below code to the example somewhere in main():

go func() {
	time.Sleep(time.Second)
	for {
		// Here we read a single line from the keyboard (os.Stdin)
		fmt.Print("Enter new title for library.book.1: ")
		reader := bufio.NewReader(os.Stdin)
		title, _ := reader.ReadString('\n')

		// Get the book resource using With
		err := s.With("library.book.1", func(r res.Resource) {
			// Get the book
			book := bookModels[r.ResourceName()]
			if book == nil {
				fmt.Println("Book not found")
				return
			}
			// Validate the title
			title = strings.TrimSpace(title)
			if title == "" {
				fmt.Println("Title must not be empty")
				return
			}
			// Check if the title is the same
			if book.Title == title {
				fmt.Println("Book already has that title")
				return
			}
			// Update the model
			book.Title = title
			// Send a change event
			r.ChangeEvent(map[string]interface{}{"title": title})
		})
		if err != nil {
			fmt.Println("With returned an error: ", err)
		}
	}
}()

A good thing to know is that with go-res there will only be a single thread handling a specific resource at any single time. What I means is, if a client tries to do call.library.book.1.set to update library.book.1, and at the very same time you try to update the book using the terminal and With, these two calls will be serialized on the same goroutine - they will never run concurrently.

Because of this, you can often avoid unnecessary mutex locks, knowing that your callback is currently the only one working with that specific resource.

Of course, another go routine might concurrently be running for another resource (eg. library.books.2), but never on the same resource.

Also, a little tip to look into. The example now has duplicate code for validating and updating the book model, code found in both the Set handler and With handler. To avoid duplication, you can use ApplyChange to add an ApplyChangeHandler that is called whenever there is a ChangeEvent. Then you can move the validation + update code there. If so, your With code above would be simplified to this:

// Get the book resource using With
err := s.With("library.book.1", func(r res.Resource) {
	// Just send a change event. The ApplyChange handler will validate
	// and update the book title.
	r.ChangeEvent(map[string]interface{}{"title": title})
})
if err != nil {
	fmt.Println("With returned an error: ", err)
}

The Set handler can then also be simplified in a similar way.

Hope that helps!

Best regards,
Samuel

#8

Hehe, that solution was much shorter than mine. But yes, that is the gist of it. :slight_smile:

Just be careful though. If you do this:

  • Update the Book title somewhere in the code
  • Use With on the resource to send a change event

… it might seem to work fine. And most of the time it does. But you might actually have introduced a race condition.

Because this can happen:

  • Update the Book title to “Foo” somewhere in the code, done in goroutine 1
  • A client calls Set, which updates the title to “Bar”. This is done on goroutine 2
  • Goroutine 1 uses With on the resources, which is queued to happen on goroutine 2 after the Set call
  • Goroutine 2 sends Title = “Bar” Change event
  • The With callback is executed and sends a Title = “Foo” change event.

Now, the service has a Book with the title “Bar”, while Resgate last received an event with title “Foo”.
Ouch.

Best thing to avoid this is to also update the Book title within the With callback, and not only send the change event. This way you avoid synchronization, such as a mutex.Lock.
Next best thing is to protect the Book behind something like a mutex.Lock.

Oh well…
Just wanted to mention it :slight_smile:

Best regards,
Samuel

#9

Hi Samuel,

Many thanks, again, for your thorough response.

Synchronization has already caught me out in my own model so I really appreciate your additional notes.

Many thanks again.
Kind Regards,
Craig Stock.