Using Asynchronous Message Bus (AMB) with Web Components, i.e. record watcher.
So the first component that I have been working on has required me to make sure that I monitor a set of records for changes so that I can update the component with new information. Now this information changes based on the current user or possibly other users in the background. So in Service Portal I would use spUtil.recordWatch to do this. Well there is no documentation that I could find on how to set this up for a component. So I went and poked ServiceNow and I was told that there was something out on npm for AMB but its usage was not supported and there were no docs that could be passed on. Well that's frustrating but it was enough information for me to find the @servicenow/ui-effect-amb and poke thru the code to start to figure it out. After poking thru that I had to spend some time looking at how a workspace actually used the method since it was not as strait forward as I thought.
There are several methods that are in the ui-effect-amb but so far I have only been working with the createAmbSubscriptionEffect and that is the one I will talk about here.
Setup
So to setup the usage of it you may need to install it.
npm i @servicenow/ui-effect-amb
For those of you who did not know the code for the ui-effect-amb is now in your project under node_modules/@servicenow/ui-effect-amb, along with all of the others that you have installed. So you can go thru the code there and see whats available whe.
Then you need to add the include for it in your code
import {createAmbSubscriptionEffect} from '@servicenow/ui-effect-amb';
Now you will see in the code I posted at the bottom I did two includes, the reason I did this was so I could dump out the object to the console and look at everything that was available by default. So you can just delete lines 4 and 7 if you are not interested in seeing that.
Usage
You would think usage would be strait forward but I guess I do not use web services enough because it was not as strait forward as I thought it should be but once I got it to work I felt it should have been at least a little obvious. So we setup the function so we know what Action Types to watch for.
const recordWatcherHandler = createAmbSubscriptionEffect('/rw/default/:table/:filter', {
subscribeStartedActionType: 'RECORD_WATCHER_SUBSCRIBE_STARTED',
subscribeSucceededActionType: 'RECORD_WATCHER_SUBSCRIBED',
subscribeFailedActionType: 'RECORD_WATCHER_SUBSCRIBE_FAILED',
unsubscribeSucceededActionType: 'RECORD_WATCHER_UNSUBSCRIBED',
messageReceivedActionType: 'RECORD_WATCHER_ITEM_CHANGED'
});
Then we can add the action type to dispatch
actionHandlers: {
'RECORD_WATCHER_SUBSCRIPTION_CHANGED': recordWatcherHandler,
}
Once we have those items its just a matter of calling the dispatch to setup the watcher. I did this using COMPONENT_CONNECTED but you could use a button or other thing to trigger your dispatch. Once we have decided when/where we are going to call the dispatch we just need to make that happen.
dispatch('RECORD_WATCHER_SUBSCRIPTION_CHANGED', {
table: state.table,
filter: state.filter ? btoa(state.filter).replace(/=/g, "-") : "",
subscribe: true
});
Notice what I did with the filter. This was the item that was eluding me until I found it in the Workspace code. After seeing this it made sense to me because the filter could be a lot of things and the REST call will not like most of them and could think that any question mark or equals was indicating something that it is not. So you need to encode the filter into a Base64 string and convert any equals sign into a dash.
Testing and issues
Testing this is fairly simple, you can use the code at the bottom to make a test component and simply change the initialState values to what ever you want. Then all you have to do is go into the instance and update a record and you will see in the console a RECORD_WATCHER_ITEM_CHANGED printed and the data passed to the effect function we setup right? Well not so much. What I found is that even after adding /amb and /rw to the proxies list in the now-cli.json file that I would get errors saying that the connection to the local host was interrupted and it could not make a connection. I could not find a way to resolve this but I did find that pushing the component to the instance and adding it to a workspace allowed it to work and I would see the action types trigger when I would insert/update/delete records that met the supplied filter.
There is an issues to be aware of. I could not find a way to distinguish one watcher from another that used the same filter. So if you have a component that loads with a form, like as a ribbon item and the component is watching the same record as the same component loaded for a different form in a different tab on workspace that they appear to use the same watcher so if you make a call to unsubscribe it does so for all of the components using that same watcher. I also noticed that if the component does load with with a form and there is already a watcher in place for the filter you are using that it does not trigger any of the action types for the second instance of the component. Also I found that when a record is inserted/updated/deleted that it does not appear to call the action type one time for each component that is loaded. Now this may be because the browser is smart enough to know that they are all rendering the same content so it does not need to do it three times it may not. So more testing is needed to understand this better.
End remarks
If you know more about this than I please pass on what you have found and post it here, I would really like to know. As I find out more I will add to this article when I can.
Complete Code
import {createCustomElement, actionTypes} from '@servicenow/ui-core';
import snabbdom from '@servicenow/ui-renderer-snabbdom';
import styles from './styles.scss';
import * as ambObj from '@servicenow/ui-effect-amb'
import {createAmbSubscriptionEffect} from '@servicenow/ui-effect-amb';
console.log(ambObj);
const recordWatcherHandler = createAmbSubscriptionEffect('/rw/default/:table/:filter', {
subscribeStartedActionType: 'RECORD_WATCHER_SUBSCRIBE_STARTED',
subscribeSucceededActionType: 'RECORD_WATCHER_SUBSCRIBED',
subscribeFailedActionType: 'RECORD_WATCHER_SUBSCRIBE_FAILED',
unsubscribeSucceededActionType: 'RECORD_WATCHER_UNSUBSCRIBED',
messageReceivedActionType: 'RECORD_WATCHER_ITEM_CHANGED'
});
const {
COMPONENT_CONNECTED,
COMPONENT_ERROR_THROWN,
COMPONENT_DISCONNECTED
} = actionTypes;
const view = (state, {updateState}) => {
return (
<div className="">
Section list:<br />
</div>
);
};
createCustomElement('mahe2-test-component', {
renderer: {type: snabbdom},
view,
styles,
actionHandlers: {
[COMPONENT_CONNECTED]: {
effect: ({state, dispatch}) => {
//Subscribe
dispatch('RECORD_WATCHER_SUBSCRIPTION_CHANGED', {
table: state.table,
filter: state.filter ? btoa(state.filter).replace(/=/g, "-") : "",
subscribe: true
});
},
stopPropagation: true
},
[COMPONENT_ERROR_THROWN]: ({...stuff}) => {
console.log(stuff);
},
[COMPONENT_DISCONNECTED]:{
effect: ({state, dispatch}) => {
// unsubscribe
dispatch('RECORD_WATCHER_SUBSCRIPTION_CHANGED', {
table: state.table,
filter: state.filter ? btoa(state.filter).replace(/=/g, "-") : "",
subscribe: false
});
},
stopPropagation: true
},
'RECORD_WATCHER_SUBSCRIPTION_CHANGED': recordWatcherHandler,
'RECORD_WATCHER_SUBSCRIBE_STARTED': (...stuff) => {
console.log(stuff);
console.log("RECORD_WATCHER_SUBSCRIBE_STARTED");
},
'RECORD_WATCHER_SUBSCRIBED': (...stuff) => {
console.log(stuff);
console.log("RECORD_WATCHER_SUBSCRIBED");
},
'RECORD_WATCHER_SUBSCRIBE_FAILED': (...stuff) => {
console.log(stuff);
console.log("RECORD_WATCHER_SUBSCRIBE_FAILED");
},
'RECORD_WATCHER_UNSUBSCRIBED': (...stuff) => {
console.log(stuff);
console.log("RECORD_WATCHER_UNSUBSCRIBED");
},
'RECORD_WATCHER_ITEM_CHANGED': (...stuff) => {
console.log(stuff);
console.log("RECORD_WATCHER_ITEM_CHANGED");
}
},
initialState: {
table: 'u_bok_section',
filter: 'u_active=true'
}
});
https://www.servicenow.com/community/developer-articles/using-asynchronous-message-bus-amb-with-web-components-i-e/ta-p/2309411