From 67264ca9412b7e21cbdc3c406129c6330d37e626 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Thu, 19 Sep 2024 16:39:15 -0400 Subject: [PATCH] Add in the dbc for JS. --- js/dbc.mjs | 33 ++++++++++ js/dbc_test.mjs | 39 +++++++++++ js/fsm.mjs | 168 ------------------------------------------------ 3 files changed, 72 insertions(+), 168 deletions(-) create mode 100644 js/dbc.mjs create mode 100644 js/dbc_test.mjs diff --git a/js/dbc.mjs b/js/dbc.mjs new file mode 100644 index 0000000..01583ed --- /dev/null +++ b/js/dbc.mjs @@ -0,0 +1,33 @@ +class DBCError extends Error { +} + +class CheckError extends DBCError {} +class SentinelError extends DBCError {} +class PreCondError extends DBCError {} +class PostCondError extends DBCError {} + +export const check = (test, msg, err=CheckError) => { + if(!test) { + console.trace(msg); + throw new err(msg); + } +} + +export const sentinel = (test) => { + check(test, "SENTINEL ERROR", SentinelError); +} + +export const pre = (test_cb, msg) => { + check(test_cb(), `PRECOND ERROR: ${msg}`, PreCondError); +} + +export const post = (test_cb, msg) => { + check(test_cb(), `POSTCOND ERROR: ${msg}`, PostCondError); +} + + +export default { + check, sentinel, pre, post, + CheckError, SentinelError, PreCondError, + PostCondError, +} diff --git a/js/dbc_test.mjs b/js/dbc_test.mjs new file mode 100644 index 0000000..099b585 --- /dev/null +++ b/js/dbc_test.mjs @@ -0,0 +1,39 @@ +import dbc from "./dbc.mjs"; + +try { + dbc.check(1 == 2, "Math is wrong!"); +} catch(e) { + console.error(e); +} + +try { + dbc.sentinel(1 == 2); +} catch(e) { + console.error(e); +} + +try { + dbc.pre(() => 1 == 2, "Pre failed!"); +} catch(e) { + console.error(e); +} + +try { + dbc.post(() => 1 == 2, "Post failed!"); +} catch(e) { + console.error(e); +} + +const test_func = (x,y) => { + dbc.pre(() => x < y, "x must be less than y"); + + let result = x + y; + + dbc.check(result > 100, "the result must be > 100"); + + result = result % 100; + + dbc.post(() => result < 100, "x was not < 100"); +} + +test_func(12, 10002343034); diff --git a/js/fsm.mjs b/js/fsm.mjs index 1c1408b..8ecaa8b 100644 --- a/js/fsm.mjs +++ b/js/fsm.mjs @@ -1,147 +1,6 @@ -/* - A very simple Finite State Machine system. If you're struggling with managing - a mixture of JavaScript API styles (Promise, callback, events) then this will - help get things organized. The best example is in the `client/components/HLSVideo.svelte` file. - - The main advantage an FSM has is it's ability to constrain and organize random events, and it's - ability to fully log what's happening in a stream of random events. The `HLSVideo.svelte` example - is a great demonstration of this because it has to juggle events from two different styles of APIs, - network events, user UI events, and __video__ loading events. Since all of these can come in at - random the FSM will help order them and do the right thing at the right time. - - ### Theory - - One way to look at a Finite State Machine is an ___explicit if-while-loop___. - If you have a `while-loop` that is doing a lot of processing based on - variables, and you're using a lot of `if-statements` or `switch-case` - statements, then the `FSM` simply formalizes exactly __what__ states and when - they can run. - - For example, if you have a mess of nested `if-statements`, and each one might - not cover every possible variable, then you'll have bugs as different - branches of the `if-statements` run or don't run. The `FSM` simply says, "The only - allowed states at this point in time are X, Y, and Z. The only events I expect are - event1, event2, and event3. If I get anything else that's an error." - - It's a different way to think about data processing, but it is very reliable _and_ - easy to debug because you can log every event, state, and all the data while processing - random events. - - ### FSMs in User Interfaces - - Most UIs are simple enough that you can get away with the basic Svelte variables updating - the state. When UIs get more complex--and especially when they trigger network events--you - run into problems with random events requiring different updates to the UI. For example, if - a user clicks on a button that loads a video, but they're already loading a different video, - then you have to deal with the current click, the old video loading state, and the new video - playing state. An FSM would be able to "serialize" the click, old video load, and new video - load and play better than the usual Svelte variables. - - In short, if you're struggling with properly updating a user interface as random events come - in from the user and the network, then switch to an FSM. - - ### Usage - - The `class FSM` takes two things: data and a event transition object. The `data` is only - for logging and debugging the FSM, as it gets printed out with each log message. The core - of the FSM is your event handler, which has functions named after each event: - - ```javascript - class VideoEvents { - async mount(state) { - switch(state) { - case "START": - return "COUNTING_DOWN"; - default: - return "ERROR"; - } - } - } - ``` - - In this snippet the `class VideoEvents` has a `async mount` event that works like this: - - + This `mount()` event takes a single `state` which is just a string for the state the FSM is currently in. - + You then figure out what to do based on this `state`, typically using a `switch/case`. - + Based on what you've done, you return the next state as another string. - + The `FSM` then waits for a new event, and calls that function with this new state. - - To trigger this `mount` event you would then write: - - ```javascript - await fsm.do("mount"); - ``` - - You can pass additional arguments to the `do` function and they will be passed to the - event as additional arguments: - - ```javascript - // if fsm is in "START" state - await fsm.do("mount", 1, 2, 3); - - // then becomes - ev.mount("START", 1, 2, 3); - ``` - - ### Logging Events - - The `FSM` class will already call `log.debug` on many things, but if you want to do your own logging - you can use the `onTransition` callback: - - ```javascript - fsm = new FSM(video_config, new VideoEvents()); - fsm.onTransition(fsm => { - video_config.state = fsm.state - log.info("I'm logging something else too", the_other_thing); - }); - ``` - - This is a slight modification of how the FSM is used in `HLSVideo.svelte`. Remember that the `video_config` is only passed to `FSM()` so that it gets logged while the `FSM` is running. This is _incredibly_ useful when debugging why your `FSM` is doing something. - - In the `onTransition` callback I'm simply updating the `state` in `video_config` and then I added a little `log.info` to show you could log anything else you wanted there. - - ### Internal Transitions - - If I'm remembering my graduate courses right, this is called an "epsilon transition", but it's - basically triggering another event inside the current event, effectively changing state without - waiting for a new event. I could be wrong on the terminology, but you do this by returning an - `Array[state, cb()]` pair. The `FSM` will then set state to `state` and call your callback so - you can do further processing. - - ___BUG___: This could be improved but for now it works. Look at `HLSVideo.svelte:detect_video` for an example of doing this. - - ### State Names - - The first state is "START" by default, but after that you can use any string. I prefer to make - the states UPPERCASE so they aren't confused with events, which are `lowercase()` function - names in the event handler class (`VideoEvents` above). - - */ - import assert from "assert"; -/* - A very simple Finite State Machine class. Nothing fancy, just a state - and some transition callbacks based on the function names found in `this.events`. - */ export class FSM { - - /* - Sets up this FSM to use the `events` callback as the transitions, and then - "START" as the first state. It does ___not___ call any initial transitions - so you have to do the first call to `FSM.do()`. The `data` isn't used - internally and only passed to logging functions for debugging. - - ### Event Class Signature - - The `events` can be anything, and you construct it so it will have your own data. - The only requirement is that the functions have a `name(state)` signature and that - `state` is going to be a string. It should then return the next state string for - the next event to receive. - - + `data Anything` -- Whatever you want to log while debugging. - + `events Event Object` -- An object/class that can handle event calls sent to it. - */ constructor(data, events) { // data is really only for logging/debugging this.data = data; @@ -150,25 +9,10 @@ export class FSM { this.on_state = undefined; } - /* - A callback that runs whenever an event handler returns a state. The `cb` callback - receives this `FSM` so it can debug it or do more to it. - - ___WARNING___: This is called called even if the state didn't change, which - might confuse people if they think it's only when a "transition" happens. - - + `cb(FSM)` -- Callback after the transition. Receives this `FSM`. - */ onTransition(cb) { this.transition_cb = cb; } - /* - Mostly used internally to perform the transition then call the `onTransition` callback, - but sometimes you might need to force a new state externally. - - + `next_state string` -- The new state name. - */ transition(next_state) { this.state = next_state; @@ -177,22 +21,10 @@ export class FSM { } } - /* - All event names available in the `this.events` class. - */ event_names() { return Object.getOwnPropertyNames(Object.getPrototypeOf(this.events)).filter(k => k !== "constructor"); } - /* - The core of this `FSM` is the `do()` function. It handles finding the next - event by it's name, loading it, and calling it with the given additional - arguments. - - + `event string` -- The __name__ of the target event, not the actual function. It's a string. - + `...args` -- Additional arguments to pass to the event. - + ___return___ `this.state string` -- The current state of this `FSM`. - */ async do(event, ...args) { const evhandler = this.events[event]; assert(evhandler !== undefined, `Invalid event ${event}. Available ones are '${this.event_names()}'.`);