Mystery Master

JavaScript Development

Michael Benson


Introduction

This help article discusses the development of the Mystery Master application in the JavaScript programming language, with references to the C# language. One of the main concerns of any application is to have a responsive User Interface (UI). If the UI is unresponsive, the user will think the application has stopped working. A C# application can get around this problem by using the async/await keywords. Until these keywords are common in JavaScript, I use the following two technologies: (1) Web Workers and (2) the setTimeout keyword.

Pros and Cons

JavaScript is the client-side language of the web. I don't think it is the best language, but there have been improvements over the years. I wish it was strongly-typed, had better scoping rules (public, private, etc.), but I guess I could use TypeScript. It can't do some of the nice things I can do in C# like initialize an object when I instantiate it. It doesn't have the null-coalescing operator ??. It is also very fragile - your IDE and/or browser may not pick up your mistakes. But enough complaining... Here are some of the things I like.

  1. The "use strict"; option.
  2. Define a constant with the const keyword.
  3. Avoid the hoisting of variables with the let keyword.
  4. The addEventListener method so the method the event calls can be local.
  5. The ability to return closures - functions that refer to local variables. See the SmartRule static class for more on closures.
  6. The ability to store/retrieve information in local storage.

Globals

Though global variables and methods are discouraged, the Mystery Master application defines the following globals.

Web Workers

A web worker (aka worker or WW) is JavaScript that runs in a background thread, without affecting the performance of the page (aka master or UI thread). By performing work in the background thread, this allows the UI thread (the page) to be responsive. The following shows how the UI creates a web worker, where WebWorker.js is the name of the JavaScript file.

let worker = new Worker("WebWorker.js");

Messages

The UI and the WW communicate via messages. The message could be a number, a string, or an object literal, which is like a structure. Here are some of the important methods to use for correspondence.

  1. The UI sends a message to the WW via the worker.postMessage(msg) method.
  2. The WW receives the message from the UI via the self.onmessage method.
  3. The WW sends a message to the UI via the postMessage(msg) method.
  4. The UI receives the message from the WW via the worker.onmessage method.

Here are some importants points about the two threads.

  1. In the master thread, the onmessage and postMessage() must be called via the Worker object, but not when used in the worker. This is because, inside the worker, the worker is effectively the global scope.
  2. The worker can import scripts via the importScripts method.
  3. Data passed between the master and worker is copied, not shared. Objects are serialized as they're handed to the worker, and subsequently, de-serialized on the other end. The page and worker do not share the same instance, so the end result is that a duplicate is created on each end.

Because the web worker must communicate with the page via messages, there is no reason to have DOM-related code in the web worker. In fact, the web worker cannot access the window object, the document object, or the parent object.

Object Literals

Messages are passed as object literals, where the first field is named key. The defined messages are given below, sorted by key.

Messages from UI to WW
  1. { "key": "addMarkByObjLit", "obj": obj }
  2. { "key": "doWork" }
  3. { "key": "loadPuzzle", "name": puzzle.myName }
  4. { "key": "quitWork" }
  5. { "key": "resetWork" }
  6. { "key": "undoUserMark" }
  7. { "key": "updateFact", "num": num, "enabled": fact.enabled }
  8. { "key": "updateRule", "num": num, "enabled": rule.enabled }

Note: Setup options for the Solver, Finder, and Lawyer are not included in the above list.

Messages from WW to UI
  1. { "key": "addMarkByObjLit", "obj": obj }
  2. { "key": "addPlaceholder", "mark": mark.num, "rule": rule.num, "nounType": noun.type.num, "noun": noun.num, "value": value }
  3. { "key": "quitWork", "msg": msg }
  4. { "key": "sayContradiction", "oldMark": oldMark.num, "markType": markType.num, "refTo": refTo, "msg": msg }
  5. { "key": "sayLawViolation", "mark": mark.num, "law": lawNum, "pos": pos, "msg": msg }
  6. { "key": "sayLevel", "num": num }
  7. { "key": "sayRuleViolation", "mark": mark.num, "rule": rule.num, "msg": msg }
  8. { "key": "saySolution", "num": numSolutions, "msg": msg }
  9. { "key": "undoAssumption" }
  10. { "key": "undoUserMark" }

Goals

The purpose of the web worker is simple; it finds the solution to the puzzle. The web worker does not need to pause; and it does not stop working until one of the following conditions is met.

  1. All solutions have been found.
  2. A fatal logic error occurred.
  3. The user clicks the Quit button.

This is in direct contrast to what the UI needs to do: The user can ask the application to pause on various conditions. To reconcile these conflicting desires, the following is performed.

  1. The worker transfers each unit of work via a message to the master.
  2. The master receives each message and places the unit of work in a queue.
  3. The master processes each unit of work in the queue, in the order it was received.

This process is similar to the Producer-Consumer model.

Producer-Consumer

First, let's call each unit of work a task. So each task created by the worker (the Producer) is sent via a message to the master. The master (the Consumer) must unpack the task given by this message, and place the task in the queue. The master must also remove each task from the queue, and update the UI by performing the task. Now, there are some timing issues that need to be worked out.

  1. The consumer may need to wait on the producer (i.e., until there is something in the queue). This is probably rare, but it could happen. This waiting must not make the UI unresponsive.
  2. The consumer may need to wait on the user (i.e., until the user presses the Resume button). The user may want to pause after each task was performed. Again, this waiting must not make the UI unresponsive.

Note: I am avoiding the terms sleep and awake. Sleep to me means that the process stops right where it's at, remembers its state, and resumes right where it left off when the awake call is issued. That certainly does not happen in JavaScript, and it doesn't happen the way I want it to in C#. You can have a thread sleep for x number of milliseconds, so this is great if you want to set an alarm. But I want some event on another thread to wake up my sleepy thread. I don't think C# can do this, but please take me "to task" if you know I am wrong.

Core Files

There are three main components: the Viewer, the Solver, and the Puzzle. The Viewer object is instantiated in the UI thread, the Solver object is instantiated in the WW thread, and the Puzzle object is instantiated in both threads. The viewer (UI) and the solver (WW) communicate via the messenging process described earlier.

Core File Thread(s) Description
Helper UI, WW Defines global objects and static methods.
NounType UI, WW Noun Type class.
Noun UI, WW Noun class.
Verb UI, WW Verb class.
Link UI, WW Link class.
Fact UI, WW Fact class.
Rule UI, WW Rule class.
Mark UI, WW Mark class.
SmartLink UI, WW Smart Link static class.
SmartRule UI, WW Smart Rule static class.
Puzzle UI, WW Base class that defines data and methods to solve the puzzle.
Solver UI, WW Calls the Finder and the Lawyer (not instantiated in UI).
Viewer UI Handles the User Interface (UI).
Locker UI Manager to save/read data from storage.
Board UI Manager to update the Board form.
Tabby UI Manager to update the tabbed interface.
Setup UI Manager to update the Setup form.
Stats UI Manager to update the Statistics form.
FileSaver UI Creates PHP files (for development only).
Finder WW Examines the facts of a puzzle to find marks.
Lawyer WW Examines marks to find additional marks.

On the UI thread, the JavaScript files are loaded via the head.php. On the WW thread, the JavaScript files are loaded in the WebWorker.js via the importScripts method. Note that the viewer and the solver are NOT aware of each other. The two threads enable separation of concerns (aka modular programming).

Load vs Init

When a JavaScript file is loaded via the script tag, the functions are loaded into memory, but are not executed UNTIL they are invoked. For example, the Verb.js file has one "class" function appropriately named Verb. This function is loaded, but is not yet executed. But after this function are static and global definitions, which are immediately executed.

General Considerations

  1. All marks MUST go through lawyer.doWork for potential violations. See "Astrophysics Conference". So do not stop just because solver.numMarks = solver.maxMarks.
  2. When the Solve/Quit/Reset buttons are pressed, a message must be passed to the WW.
  3. When Solver options are updated in the Setup form, they must be passed to the WW.

UI Considerations

The following topics concern the UI.

setTimeout

To resolve the wait issue in JavaScript, I use setTimeout. This turns a procedure-based program into an event-driven one. After the consumer performs a task, it must see if it needs to wait. If it does not need to wait, it simply moves on to the next task in the queue. But if it needs to wait, instead of spinning CPU cycles in a loop until the user presses the Resume button, the consumer simply stops running. This is the Not Runnable state. Now the only way for the consumer to resume is to have the click event of the Resume button put the consumer into the Runnable state.

Pausing

Pausing is a concern only on the UI thread. Therefore, the marks that are entered into the queue via the solver.addMark method on the master thread must not update the viewer. Instead, the master thread needs to separately call the viewer for each mark in the queue because it may need to pause until the user clicks the Resume button. The user may request the program to pause in the situations given below.

Board Buttons

Here are all of the possible states for the Work and Quit buttons.

  1. When there is no solver or no puzzle, both buttons are disabled and labeled "Solve" and "Quit".
  2. When the puzzle is loaded, the Solve button is enabled.
  3. When the Solve button is pressed, the Solve button says "Pause" and the Quit button is enabled.
  4. When the Pause button is pressed, the Pause button says "Resume".
  5. When the Resume button is pressed, the Resume button says "Pause".
  6. When the Quit button is pressed, the Pause button says "Solve" and the Quit button says "Reset".
  7. When the Reset button is pressed, the Reset button says "Quit" and is disabled.

Warning: For DOM objects with an id, most browsers will auto-create JavaScript variables with the id as its name! I don't like this because I do not want the viewer to access fields that are managed by subcomponents.