monitor.register()
Define and register custom monitorable types using monitor.register(), to be monitored manually using monitor().
Warning: This is an extremely low-level function. It is not to be used lightly and requires a full understanding of monitoring.
Syntax
monitor.register(type, definition);
Parameters
- type
-
A string for the custom type. The types 'undo' and 'live' are registered already and cannot be overwritten.
- definition
-
A class with certain properties and methods describing how the type should be monitored. The class is instantiated once for each monitored context that monitors for type. It should have the following interface:
- result
- The key under which the resulting accumulated monitored items are found. This should be an object or function reference, and should not itself change over the course of a monitored context (though properties on said object or function may change).
- add()
- A method, called once every time an item is added to the monitored context using
monitor.add(). It receives the arguments passed to the respective monitor.add() call, excluding the first argument (which specifies the type).
- until() optional
- A method, called right before
until() wants to resume a monitored context. It should return a boolean value; if it returns true, then until() does not continue the monitored context. If it returns false, then until() resumes the monitored contexts as usual. Omitting the method altogether is equivalent to having it always returning false.
Return value
None (undefined).
Details
The monitor.register() function exposes the inner workings of monitoring types "undo" and "live", and lets authors build on the monitoring system already in place. While registering new types does not directly interact with monitored contexts Yozo creates, it can be used in conjunction with monitor() and monitor.add() to create completely custom systems that "see" the use of specific functions or items. It is strongly recommended that references to monitor() and related functionality is abstracted away behind wrapper functions or classes for a more streamlined developer experience.
Types may be registered later in the life of a document, but existing monitered contexts do not retroactively use a defined type, since the call's result object for each type has already been created.
It is not possible to overwrite or update types (other than modifying the definition class); calls to monitor.register() with a type argument that was already registered are ignored. Furthermore, while it is technically possible to register the type 'result', this must not be done since monitor() overwrites the .result key on its return value with the return value from the callback function. In other words, 'result' could be registered as a monitorable type, but cannot practically be monitored.
Examples
Monitoring undo
We'll have a look at what a re-implementation for the monitored type "undo" might look like. Note that the actual implementation differs from this implementation since it optimizes for bundle size whereas this example focuses more on readability. First, we need to define the class responsible for handling and aggregating the cleanup callbacks for each monitored context. An instance of this class is created every new monitored context.
class AlsoUndo {
#stopped = false;
#callbacks = [];
add(callback){
if(this.#stopped){
callback();
} else {
this.#callbacks.push(callback);
}
}
result = () => {
if(this.#stopped) return;
for(const callback of this.#callbacks){
callback();
}
this.#callbacks = [];
this.#stopped = true;
}
until(){
return this.#stopped;
}
}
We need to keep track of two primary things; a boolean #stopped indicating whether or not we have "undone" the monitored context in question, and an array of cleanup #callbacks. The array aggregates the callbacks from the monitored context, so they may be run when the context is undone.
The add() method is responsible for adding the callbacks to the context. We need to handle both the case of an already-undone monitored context (in which case we fire the callback immediately) and one that has not yet been undone (for those, we add the callback to our #callbacks array).
Next, the result key. It is very important that the result key is never changed, since it is only read once when a monitored context is created. This is the object (or, in this case, function) that is exposed in the return value of monitor() under the key with the name matching the monitored type's name. Our implementation here early-returns if the monitored context was already undone, since the cleanup should only ever happen once. Otherwise, we run the callbacks, clean up the references to them, and set the #stopped property.
Lastly, we add an until() method, with which we can tell the same-named until() function to stop resuming the monitored context. Conveniently, we only need this to happen when this.#stopped is true, and therefore we can return it directly.
Now, to register it, we need to call monitor.register() and pass it a type (let's go for 'alsoUndo') and the AlsoUndo class we defined above:
monitor.register('alsoUndo', AlsoUndo);
Now, the type is ready to be monitored. For the sake of the example, we'll use monitor.add() and monitor() directly, but it is advised to abstract these calls away in higher-level functions when used in real projects.
const call = monitor(['alsoUndo'], () => {
// code…
monitor.add('alsoUndo', () => {
console.log('undone!');
});
// more code…
});
Then, since the type is called 'alsoUndo' we can later call call.alsoUndo() to run the cleanup callbacks. The call.alsoUndo() function is the same one we've defined under the result key in our AlsoUndo implementation.
See also