We are the music makers. We are the dreamers of dreams.

“Willy Wonka & the Chocolate Factrory” (1971)

We think in language. The quality of our thoughts & ideas can only be as good as the quality of our language.

☛ George Carlin

Be a yardstick of quality. Some people aren't used to an environment where excellence is expected.

☛ Steve Jobs

There is a presumption that it is not our job to produce good software but that it is our job to just produce features without attention to detail or quality. This clearly doesn’t make any sense when you look at it from an economic point of view.

☛ Kevlin Henney

Rethinking Base64

Sukima25th October 2025 at 11:37am

I recently had a need to couple compression and base64 encoding with built-in tools (no 3rd party libraries). The trouble is that these two ideas don't have an easy common connection. What I mean by that is that Web APIs expose built-in compression as a Stream. While base64 is exposed as string only synchronous functions.

The difference between them stems from the different use cases. Compression is designed to work with binary data while base64 historically is designed for ASCII strings.

There is a lot to get into and a good way to do that is to walk through a working example.

I have a side project I call fiddles (source). It is a simple app, allow users to code in a text editor and reflect the result inside an iframe.

I wanted to add support for PlantUML which has a service that can render PlantUML source into an image. To accomplish this they want the source to be compressed and encoded in a very specific way and added as part of the URL path.

For example, this source:

@startuml
A -> B: Hello
@enduml

Is compressed and encoded: SoWkIImgAStDuN9KqBLJSB9Iy4ZDoSddSaZDIm6A0W0

Construct that to a URL: https://www.plantuml.com/plantuml/svg/SoWkIImgAStDuN9KqBLJSB9Iy4ZDoSddSaZDIm6A0W0

Results in this image:

Result of rendering PlantUML source through the PlantUML service

This encoding process ends up being five parts.

  1. Convert a string to a stream of bytes
  2. Compress the stream of bytes
  3. Encode the stream of bytes to base64
  4. Transform the base64 encoding to a PlantUML compatible encoding
  5. Convert the stream pipeline back into a string

Convert a string to a stream

Since we need a stream because the only compression support browsers have is via CompressionStream.

There are a few ways to convert a string to a stream of bytes. My first thought was to convert a string to a Uint8Array using TextEncoder. But I'd still have to convert the Uint8Array into a stream.

My next idea was to use TextEncoderStream as that seems like exactly what we want. But… I'd still have to make a custom ReadableStream to convert the string to a stream I can pipe to the TextEncoderStream.

Ok maybe ReadableStream.from() could do that. And this would be the answer except ReadableStream.form() is only available in FireFox (for now). To support this we would still have to use a polyfill.

Luckily we do have a widely supported way to convert a string to a byte stream through a Blob.

new Blob(['foobar']).stream();
// => ReadableStream<Uint8Array>

Compress a stream of bytes

With a stream of bytes we easily pipe it through the built-in compression transform stream.

new Blob(['foobar']).stream()
  .pipeThrough(new CompressionStream('deflate-raw'));

The currently supported compression algorithms are: gzip, deflate, and deflate-raw. In our case PlantUML expects deflate-raw not needing any identifying header bytes.

The difference between deflate and deflate-raw is the same except deflate adds some identifying header bytes.

Encode into base64

Here is the fun part. There is not a built-in way to stream encode base64. Instead we have to either use btoa() or process it into a data: URI process.

A bunch of examples use the data URI method as it doesn't have the awkward string to string system that btoa() has and doesn't fall prey to the foot-guns of UTF-8 code points causing btoa()/atob() to crash.

However, the implementation is not strait forward and it kind of obfuscates the intent. As well they have very divergent implementations between encoding and decoding.

There are also many examples where people implemented the base64 bit math themselves.

I found, however, that with a slight bit of math and clever use of chunked string/array processing it was possible to use btoa()/atob() effectively without the complexity of the other solutions.

class Base64EncoderStream
extends TransformStream<Uint8Array, string> {
  constructor() {
    let buffer = new Uint8Array();

    super({
      transform: (chunk, controller) => {
        const bytes =
          new Uint8Array([...buffer, ...chunk]);
        const split = bytes.length - (bytes.length % 3);
        const next = bytes.slice(0, split);
        buffer = bytes.slice(split);
        controller.enqueue(this.encode(next));
      },
      flush: (controller) => {
        if (!buffer.length) return;
        controller.enqueue(this.encode(buffer));
      }
    });
  }

  encode(bytes: Uint8Array): string {
    return btoa(String.fromCodePoint(...bytes));
  }
}

Deep dive into Base64 encoding process

Base64 is an encoding system, that inputs a set of bytes and outputs a set of strings. In this case we break up the set into chunks. When encoding it consumes three bytes at a time and outputs a chunk of four characters.

Input chunk size Output chunk size
Encoding3 bytes4 characters
Decoding4 characters3 bytes

While encoding to compensate for chunks that fall short of the chunk size padding (the = character) is added to the end of the output. For decoding padding is ignored.

  1. We take each chunk and combine it with any bytes that might have been left over from a previous chunk
  2. We find the largest set of three bytes in the buffer
  3. We split the buffer at that point creating two byte arrays
  4. The last set is saved for later processing as it is not three bytes in length
  5. The first part which will always be a factor of three bytes is encoded using fromCharCode and btoa

Because the first byte array is always a factor of three bytes the encoding will never include a trailing = character which allows each chunk to be concatenated together safely later on.

The reverse works for decoding as long as we use strings that meet a factor of four characters.

Add the encoder to the pipeline:

new Blob(['foobar']).stream()
  .pipeThrough(new CompressionStream('deflate-raw'));
  .pipeThrough(new Base64EncoderStream());

Transform to PlantUML compatible encoding

PlantUML uses a different string encoding than Base64. Luckily, its encoding is also a 64 character set. Since the two encodings have the same sized encoding set we can post transform from Base64 to PlantUML encoding.

Base64ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef ghijklmnopqrstuvwxyz0123456789+/
PlantUML0123456789ABCDEFGHIJKLMNOPQRSTUV WXYZabcdefghijklmnopqrstuvwxyz-_

A transform is a matter of mapping the Base64 character with PlantUML's equivalent character and stripping out any trailing = padding.

const base64map =
  'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef' +
  'ghijklmnopqrstuvwxyz0123456789+/';
const plantUmlMap =
  '0123456789ABCDEFGHIJKLMNOPQRSTUV' +
  'WXYZabcdefghijklmnopqrstuvwxyz-_';

export class PlantumlEncoderStream
extends TransformStream<string, string> {
  constructor() {
    super({
      transform: (chunk, controller) => {
        const lookup = (char: string) =>
          plantUmlMap[base64map.indexOf(char)] ?? '';
        const encoded = chunk.replaceAll(/./g, lookup);
        controller.enqueue(encoded);
      },
    });
  }
}

Add the encoder to the pipeline:

new Blob(['foobar']).stream()
  .pipeThrough(new CompressionStream('deflate-raw'));
  .pipeThrough(new Base64EncoderStream());
  .pipeThrough(new PlantumlEncoderStream());

Convert the stream back into a string

Once a stream it is streams all the way down. The end needs to collect all the chunks into a single basket. To do that we can make a small WritableStream that can collect strings and concatenates them for consumption when the stream completes.

export class CaptureStringStream
extends WritableStream<string> {
  result = '';
  constructor() {
    super({
      write: (chunk) => {
        this.result += chunk;
      },
    });
  }
}

Add the writer to the pipeline:

const captureStream = new CaptureStringStream();

await new Blob(['foobar']).stream()
  // => Uint8Array(6) [102, 111, 111, 98, 97, 114]
  .pipeThrough(new CompressionStream('deflate-raw'));
  // => Uint8Array(12) [
  //      74, 203, 207, 79, 74,
  //      44,   2,   0,  0,  0,
  //      255, 255
  //    ]
  // => Uint8Array(2) [ 3, 0 ]
  .pipeThrough(new Base64EncoderStream());
  // => "SsvPT0osAgAAAP//"
  // => "AwA="
  .pipeThrough(new PlantumlEncoderStream());
  // => "IilFJqei0W000F__"
  // => "0m0"
  .pipeTo(captureStream);

console.log(captureStream.result);
// => "IilFJqei0W000F__0m0"

Final step: build the URL

The last step is to build this into a URL.

const imageSrc = new URL('https://www.plantuml.com');
imageSrc.pathname =
  `/plantuml/svg/${captureStream.result}`;

See it working in your browser

Discuss this article

Visualizing data in source code with string parsing

Sukima7th February 2024 at 7:54pm

The other day I stumbled on a piece of code that really threw me for a loop. It had to do with constructing a data structure that included positioning data for use in a grid layout.

function buildCells() {
  return [
    {
      key: 'foo',
      label: 'Foo',
      value: 'This was a foo value',
      position: [0, 0],
    },
    {
      key: 'bar',
      label: 'Bar',
      value: 'This was a bar value',
      position: [0, 1],
    },
    {
      key: 'baz',
      label: 'Baz',
      value: 'This was a baz value',
      position: [1, 1],
    },
  ];
}

Which would cause the rendering engine to position boxes for each entry at the coordinates outlined by the position values.

The trouble came in when that structure was dynamic. At first the idea would be to save the basic array to a variable then mutate it as conditions came to light.

function buildCells() {
  let result = [
    {
      key: 'foo',
      label: 'Foo',
      value: 'This was a foo value',
      position: [0, 0]
    },
    {
      key: 'bar',
      label: 'Bar',
      value: 'This was a bar value',
      position: [0, 1]
    },
    {
      key: 'baz',
      label: 'Baz',
      value: 'This was a baz value',
      position: [1, 1]
    }
  ];

  if (condition1) {
    result.splice(0, 1);
  }

  if (condition2) {
    result.splice(1, 1);
    result[0].position = [0, 0];
  }
}

And if I am honest I got very confused very quickly. I couldn’t tell which slot each cell was going to be in or why.

I did what I usually do and broke it down into parts. In these kinds of builders sometimes I find using generator functions helps because the control flow is explicit.

function *buildCells() {
  if (condition1) {
    yield {
      key: 'foo',
      label: 'Foo',
      value: 'This was a foo value',
      position: [0, 0]
    };
  }

  yield {
    key: 'bar',
    label: 'Bar',
    value: 'This was a bar value',
    position: condition2 ? [0, 0] : [0, 1],
  };

  yield {
    key: 'baz',
    label: 'Baz',
    value: 'This was a baz value',
    position: [1, 1]
  };
}

But to be frank that just felt like I was shuffling dirt around and not really cleaning it up. What I really wanted was a way to express the actual expected cell positions in a way that was easy to see in the code.

Then it hit me, what if I had a syntax that could visually demonstrate what I really needed.

function buildCells() {
  if (condition1) {
    return Array.from(buildLayout(cellsByKeyword, `
      ... bar
      ... baz
    `));
  } else if (condition2) {
    return Array.from(buildLayout(cellsByKeyword, `
      bar ...
      ... baz
    `));
  } else {
    return Array.from(buildLayout(cellsByKeyword, `
      foo bar
      ... baz
    `));
  }
}

Something like that would be far easier to reason about. I was afraid that something like that would be to complex but it turns out the algorithm isn’t all that bad.

The setup is to move the data into a lookup table.

const cellsByKeyword = {
  foo: {
    label: 'Foo',
    value: 'This was a foo value',
  },
  bar: {
    label: 'Bar',
    value: 'This was a bar value',
  },
  baz: {
    label: 'Baz',
    value: 'This was a baz value',
  }
};

Then the algorithm:

function *buildLayout(cellsMap, layout) {
  let rows = layout.trim().split('\n');
  for (let row = 0; row < rows.length; row++) {
    let columns = rows[row].trim().split(/\s+/);
    for (let column = 0; column < columns.length; column++) {
      let key = columns[column];
      let cell = cellsMap[key];
      if (cell) yield { ...cell, position: [row, column] };
    }
  }
}

Demo

Discuss this article

Page Unload Management

Sukima15th January 2024 at 12:40pm

Something that often gets overlooked in web based apps is how to manage preventing the user from leaving the app before it has had a chance to finish saving the dirty data. This is complicated by the fact that there are two entry points to handle this depending on what the user does and that its use is tightly coupled to the form(s) data handling a dirty state.

Even writing that sentence above has me feeling like this topic is complicated regardless of the simplicity we see in the many stack overflow conversations. When I’m faced with this in my own project I will often push the specifics into its own abstraction. I call this an UnloadManager because I lack any ounce of creative naming skills. What this class does is register an unload event listener for browser use, provide a hook the app code can call to present the user an in-app based confirmation, and offer an API to toggle the dirty state of the app.

With that kind of interface the app can tap into the unload manager to perform the complex aspects of managing the different ways the app/browser can interact with the user about asking them “are you sure you want to abandon dirty changes?”

Thus there are three aspects to this interface:

  1. The app’s dirty state ― for knowing when it is appropriate to interrupt the user to confirm their intention to abandon changes
  2. The browser’s beforeunload event ― for when the user performs a page refresh or moves away from the current page/app
  3. The app’s confirmation system ― for when the user performs and action that triggers the app’s internal routing system. For example: closing a dialog and navigating between app sections (without refreshing the page itself)

Dirty state

Most of the frameworks I’ve seen can provide for a resource that your app code hooks into. In Ember this is called a service In VanillaJS this could be a class instance in the main app module. A place to store the state of if the app is dirty (what ever that means) and that state can be used to make the decision to ask the user for confirmation or not when they navigate away from their current focus. Thus something very simple will do for now, a boolean field and some fancy named methods to make the intention well known.

export class UnloadManager {
  isDirty = false;

  dirty(): void {
    this.isDirty = true;
  }

  reset(): void {
    this.isDirty = false;
  }
}

Handling beforeunload

With the state managed we can use that to turn on and off the beforeunload handler. That way it is out of the way normally but will request the browser to confirm before leaving the page.

The beforeunload is a bit of magic because it doesn't work the same as normal event handlers and it is different for every browser. I found the following pattern works well in most browsers.

export class UnloadManager {
  // …

  constructor(win = window) {
    win.addEventListener(
      'beforeunload',
      this.handleUnload,
      this.cleanup,
    );
  }

  dispose(): void {
    this.cleanup.abort();
  }

  private cleanup = new AbortController();
  private handleUnload =
    (event: BeforeUnloadEvent): string | undefined => {
      if (!this.isDirty) return;
      event.preventDefault();
      event.returnValue = '';
      return 'unsaved changes';
    };
}

Might be worth describing some of the things going on here. The handleUnload is an arrow function so the this context is not lost when we attach the event. In it we check the isDirty value and either do nothing (the user never dirtied the app) or it will prevent the default (I think this is for Firefox), set the returnValue to an empty string (this is for Safari), and return a non-empty string message (this is for Chrome). When these things happen in a beforeunload event the browser will present the user its own type of dialog confirmation.

When the class is constructed we attach the event and also provide a way to detach the event in cases where we may need to instantiate and dispose of multiple unload managers like we would during testing. I like to think that when I tickle global state it will lead to memory leaks if I am not a nice citizen and offer a way to clean up after myself.

Using AbortController for event detachment management

A clean way to handle event attachment is to use an AbortController because its interface is designed to abort whatever its signal is attached to. In older versions of JavaScript we had to keep a reference to our handler function to call removeEventListener or design some kind of closure system. But with the AbortController we can do this in a more abstract declarative way.

This is different then the use of once because that requires the event to be triggered in order for it to be cleaned up (detached) We also don’t want prematurely detach till we request it from dispose().

In this case instead of a reference to the bound function and repeating the calling arity we can pass it a signal that will auto-detach when the controller is aborted. I found it far cleaner.

OK ✗

let handler1 = () => { … };
let handler2 = () => { … };
let handler3 = () => { … };

document.addEventListener('…', handler1);
document.addEventListener('…', handler2);
document.addEventListener('…', handler3);

function cleanup() {
  document.removeEventListener('…', handler1);
  document.removeEventListener('…', handler2);
  document.removeEventListener('…', handler3);
}

Better ✓

let controller = new AbortController();

document.addEventListener('…', () => { … }, controller);
document.addEventListener('…', () => { … }, controller);
document.addEventListener('…', () => { … }, controller);

let cleanup = () => controller.abort();

User confirmations

The above will prevent the user from closing the browser or leaving the site but there is a trade off here. First, the UI is out of our control, the browsers offer a generic message and that is all. Second, it only comes into play if the navigation changes/updates the browsers URL―in essence a full new page to unload and re-load.

In the case of in-app navigation we would want a different kind of confirmation. For example perhaps we may want a modal dialog box to display. We can accomplish this by offering a method that can show a confirmation or not based on the isDirty state.

There is a lot to doing this, especially since modal dialog UIs are difficult to do well and are asynchronous by nature. I’ve gone into detail about modal dialog boxes and the use of a modal manager pattern. If we use this pattern it offers a generic and flexible way to accomplish this. The basic interface is an open() method that returns Promise<ModalResult> where the ModalResult is a POJO with a reason and a value. The reason is one of: confirmed, cancelled, or rejected. That will allow us to compose our dialog management without unload management.

interface ModalResult {
  reason: 'confirmed' | 'cancelled' | 'rejected';
  value?: unknown;
}

interface ModalManager {
  open(): Promise<ModalResult>;
}


export class UnloadManager {
  // …

  async confirmation(modal: ModalManager): Promise<ModalResult> {
    if (!this.isDirty) return { reason: 'confirmed' };
    let result = await modal.open();
    if (result.reason === 'confirmed') this.reset();
    return result;
  }
}

To see this in action try the full implementation for yourself: DEMO

Discuss this article

Simple DSL Via XML

Sukima14th December 2023 at 9:22pm

There I was deep in some code designed to work with a specialized data structure: a flat tree or graph. "What is that?" You might ask.

I have no idea if this is an actual term but I am calling it a flat tree to describe a data model that represents a tree but is a single array where each entry has a reference to its parent by an ID.

[
  { "id": "1", "value": "foo" },
  { "id": "2", "value": "bar", "parent": "1" },
  { "id": "3", "value": "baz", "parent": "2" },
  { "id": "4", "value": "frotz", "parent": "1" }
]

Example of a flat tree as JSON

With this diagrammed we see the relationships:

Example of a tree structure

This is a simple example but imagine the graph being much more complex. On the project I was working on I needed to generate such data in tests. The issue was that I had to work harder to map the values to reach the same tree I had in my mind.

What I really wanted was a way to describe the tree as nested nodes like it was in my mind and have the computer correctly generate the properties to reflect that tree.

At first I thought about a fluent interface design but in TypeScript I worried about the complexity growing fast. It also didn't describe nesting well without tons of callback functions to describe the scoping.

My next thought was LISP-like which uses S-Expressions to describe lists and trees. An advantage of this kind of language is how easy it is to parse it. The disadvantage is that many really find the syntax difficult to read. It also means writing a parser on myself.

My third thought was XML as it is designed specifically to describe trees. It is also relatively recognizable in the front end because HTML. And best of is that parsing XML is built-in to all browsers as a one-liner!

<node id="1" value="foo">
  <node id="2" value="bar">
    <node id="3" value="baz"/>
  </node>
  <node id="4" value="frotz"/>
</node>

Example of a tree structure in XML

Neat, now I can describe a nested structure, parse it with built-in tools, and have it generate a proper data structure from that language.

test(
  'it parses XML into a flat tree structure',
  function (assert)
  {
    let expected = [
      { id: '1', value: 'A', parent: undefined },
      { id: '2', value: 'B', parent: '1' },
      { id: '3', value: 'C', parent: '1' },
    ];
    let actual = tree`
      <node value="A">
        <node value="B"/>
        <node value="C"/>
      </node>
    `;
    assert.deepEqual(actual, expected);
  }
);

Example test code using this DSL

If you notice, The example uses tagged template strings making the syntax even more terse then calling a function. Not only that but it can handle string interpolation and multi-lines well. Being XML whitespacing is not a concern and you can have comments inline.

Getting to the implementation… we can start with the entry point. Define the tagged template string function:

interface TreeNode {
  id: string;
  value: string;
  parent?: string;
}

function tree(
  strings: TemplateStringsArray,
  ...parts: unknown[]
): TreeNode[] {
  // TODO: not implemented yet
  return [];
}

When you call a tagged template string like this it will provide the function an array of strings and a variable number of args for each interpolated part.

tree`foo${'bar'}baz${'frotz'}foobar`;

Results in…

tree(['foo', 'baz', 'foobar'], 'bar', 'frotz');

Thus we can combine this into a single string with this one-liner:

let xml = strings.reduce(
  (r, s, i) => `${r}${s}${parts[i] ?? ''}`,
  ''
);

With a full string we can pass that directly into the XML parser with this:

let parser = new DOMParser();
let doc = parser.parseFromString(xml, 'application/xml');

The second argument to parseFromString can be either application/xml or text/html. In our case we want the parsing to be strict since the data it represents is also strict in nature. If we were doing document based markup the text/html would be more inline with our needs.

function tree(
  strings: TemplateStringsArray,
  ...parts: unknown[]
): TreeNode[] {
  let xml = strings
    .reduce((r, s, i) => `${r}${s}${parts[i] ?? ''}`, '');
  let doc = new DOMParser()
    .parseFromString(xml, 'application/xml');
  // TODO: not there yet
  return [];
}

At this point we have a fully fledged document object we can query and mutate as needed.

For example we could add auto-generated IDs to every node if we wanted:

for (let node of doc.querySelectorAll('node'))
  node.setAttribute('id', String(++nextId));

Given that this is a nested structure we can use recursion to generate objects for each node.

Also note, that an XML document requires a root node or it won’t parse properly that means we will always have a top level node to use recursion on. And better yet, walking a DOM is done top down which is the order we need when it comes to linking each child node to their parent.

Another trick with JavaScript is the ability to manage iteration with recursive generator functions.

return Array.from(nodesFromElement(doc.documentElement));

The nodesFromElement generator function could look like this:

function* nodesFromElement(
  node: Element
): Generator<TreeNode> {
  yield validNodeFromElement(node);
  for (let child of node.children)
    yield* nodesFromElement(child);
}

Given an Element we convert it to a data node and yield it. The we loop over the children recursively calling this function and yielding each result.

function validNodeFromElement(node: Element): TreeNode {
  switch (node.tagName) {
    case 'node':
      return nodeFromElement(node);
    case 'parsererror':
      throw new SyntaxError(
        node.textContent ?? 'malformed XML'
      );
    default:
      throw new SyntaxError(`unknown tag ${node.tagName}`);
  }
}

In this function we check the node's tag name to make sure we only work on the Domain Specific Language we’ve sanctioned.

We also check for XML parsing errors because unlike most functions that throw exceptions, DOMParser fails quietly by generating a document with a <parsererror> tag in it. We use that as a reason to throw our own exception.

Our last step in this process is to convert a valid node into a data structure which is eventually yielded.

function nodeFromElement(node: Element): TreeNode {
  let parentId = node.parentElement?.getAttribute('id');

  return {
    id: node.getAttribute('id') ?? '',
    value: node.getAttribute('value') ?? '',
    parent: parentId ?? undefined,
  };
}

See the full DEMO

Discuss this article

Pair programming: A critique

Sukima2nd November 2023 at 8:30am

I had a tiff the other day about the merits of pair programming. I’ve had the mantra of pair programming drilled into my head by prominent industry leaders like Martin Fowler and Dave Farly who proclaim the benefits proudly. And I personally agree; my view is obviously biased.

In a fit I thought what would happen if I set AI on the task of summarizing the pros and cons between pair programming and code reviews and I was surprised not by the results but by some of the assertions it came up with. That was the motivation to discuss some of those assertions here.

For starters you can read the essay produced by ChatGPT (noting the obviously biased prompt). I will highlight some of the assumptions here and (I hope) critique them.

Pros of Pair Programming:

Immediate Feedback: Pair programming provides instant feedback, allowing issues to be addressed as they arise. This results in cleaner code with fewer bugs and potential vulnerabilities.

ChatGPT essay

I take issue with this; to me “clean code” and “fewer bugs” is more a byproduct of experience then it is a byproduct of pair programming directly. Though I suppose it could be said that pairing with someone who has experience might produce such an outcome. But I do not think it is an inherent result of pair programming.

Enhanced Learning: The real-time interaction between the driver and navigator facilitates knowledge transfer. It promotes continuous learning and mentorship within the team, making it an excellent educational tool.

ChatGPT essay

This I do agree with. My ability to teach has always been most effective one on one in real time. Most of my research into pair programming suggests the same knowledge transfer is most effective through synchronous communications. This also leads to hands on experimentation; implicit and live brainstorming.

Reduced Debugging Time: By catching and addressing problems early, pair programming ultimately reduces the time spent on debugging, leading to more efficient code development.

ChatGPT essay

Again, I think this is a misnomer as I stated above I do not think this is a direct result of pair programming but more of a bonus side effect.

Good debugging skills can be learned independently; pair programming is not a requirement for this, nor does it enhance it directly.

I believe that the act of pair programming might suit well for managing debugging skills. And I endorse this very much.

Increased Collaboration: Collaboration improves team dynamics and fosters a sense of shared ownership, ultimately leading to better code quality and a sense of collective responsibility.

ChatGPT essay

Now, this I can adamantly support. The code I have been the most proud of has been code produced during pair programming sessions.

Cons of Pair Programming:

Reduced Autonomy: Some developers prefer working independently, and pair programming might be perceived as restrictive or inefficient.

ChatGPT essay

I can’t argue must with this. Though I personally feel it is a myth; it is something I have head from others.

I have not personally experienced any perceived restriction or inefficiencies while pairing.

Higher Resource Costs: It requires two developers to work on the same task simultaneously, which can be more expensive than having one developer work independently.

ChatGPT essay

This is a controversial opinion but I honesty believe this is incorrect.

Specifically, the cost of one developer performing tasks in isolation. Plus the cost of false starts, incorrect assumptions, etc.

Though the same exploration happens in both solo and pairing; in isolation These costs are larger then if the same were explored in pair programming. This is because with pairing the feedback is faster. Going solo means running a train of though to completion, writing hundreds of lines of test code, and painting yourself into to a corner, all before you get to publish the work for feedback. Then you add on the cost of someone else needing to get up-to-speed and cognitively understand the entire set of changes; again, before they can provide constructive feedback.

By comparison, pair programming has a far shorter feedback cycle. Allowing for experimentation while minimizing any time and energy lost.

Pros of Code Reviews:

Diverse Perspectives: Code reviews involve multiple team members, providing various perspectives on the code, which can lead to more comprehensive feedback and improved code quality.

ChatGPT essay

I don’t dispute these claims but again it leads to a resourcing mismatch. This claim presumes that there are multiple team members all code reviewing. And I take issue because in my experience this is not the case. I’ve seen more culture rules and expectations forcing people to review to get code merged. This is because by default reviews are lower on the priority chain then features.

I’m sure this idea looks good on paper and ideally is what we would want. I just don’t see it happen this way all that often.

Flexibility: Developers can work on different tasks simultaneously and submit code for review when they are ready, allowing for greater flexibility in the development process.

ChatGPT essay

I won’t try to dispute this one. I agree, code reviews compared to pair programming will offer more time management flexibility.

I will say that, the pairing sessions I’ve done have completed ahead of schedule. I can’t explain why and there is little I can cite on the topic.

Documentation: Code reviews create a record of feedback and decisions, making it easier to track changes and maintain a history of code modifications.

ChatGPT essay

Again, I agree that you do loose some documented conversations that you can capture in code reviews.

I could also counter a little and say that a good pairing will also produce documentation as the pair take notes, construct quality commit messages, and write actual technical docs in comments and wiki entries.

It has been my experience that if the culture promotes such expectations the overall team output quality increases with pair programming culture compared to code review culture.

Cons of Code Reviews:

Delayed Feedback: Code reviews occur after the code is written, which may lead to delays in identifying and fixing issues, potentially resulting in lower code quality.

ChatGPT essay

This is my biggest gripe with code reviews. The feedback cycle is excruciatingly slow. And I would go so far to claim the quality of feedback after the fact is much lower then what we could get with a shorter feedback cycle.

I'll often have to rewrite ideas in a generalized form and post it to with a “which code example is good?” Hoping to get contextual feedback only to have hundreds of slack messages trying to explain why I even bothered posting in the first place.

Removing the context so you can make quick feedback cycles while developing always leads me to disappointment as the advice is less contextual and I spend more time adding context back in just to get poor quality feedback anyway.

By comparison, when pair programming my feedback for “what about this…” is immediate, soaked in context, and mutually understood.

Less Educational: While code reviews can offer valuable feedback, they lack the interactive and educational aspects of pair programming, as they often involve static feedback provided in written form.

ChatGPT essay

It is often that education is viewed as a money pit ― I believe. I think this is because education is a long term pay-off not an immediate pay-off to direct customer sales.

This is a sad commentary of our industry.

Additional Commentary

I found other reasons to like pair programming that I don’t see discussed all that often. My largest likes for pairing are some of the following:

Mutual exploration
Exploring the complexities of a system can help to have a life line available. One to explore the other to keep the big picture in mind. Much like having known working code available to compare against while refactoring code, having known working mental models helps in comparisons of different approaches and helps tease apart good and poor abstractions.
Mutual failure experiences
Failing at something can be hard to process. We like to avoid it. Doing so with another allows for easier management of failure because you get emotional support during. And you also get emotional support after when attempting to justify such false starts and dead ends. If one person spins their wheels it can be perceived as a failing of the individual; two people spinning their wheels on the same problem can be perceived as an inherently complex problem―cost of doing business.
Parallelized research
Often going solo I'll loose time or have to context switch because I need to research a topic. Questions come up a lot: “Does this thing work?”, “What type does that method return?”, “Is that feature supported in our target environments?”, etc. Having a collaborative partner means that research can happen in parallel to the flow of brainstorming and coding. While one might type the boilerplate code the other is searching Google. There are reasons we have research groups.
Distraction management
Slack, emails, bathroom breaks, etc. These don’t have to distract you from continuing a train of thought. Manager wants an update―one pair partner can respond while the other continues to write that class interface. Not sure what to say―brainstorm. Being able to collaboratively respond and react to external distractions is a very empowering experience and lowers the context switching as the context can remain with one while other contexts are managed concurrently.
Shared context
It would be naive to assume that everyone has the skills to clearly articulate a large complex context in a single Pull Request summary or commit messages. It would be even more naive to assume everyone was just as skilled at understanding such communication. What is not naive is spreading that context among more than one person to get a more well rounded understanding and eventually better articulation and communication from them. This is evident in mob programming―though maybe not as practical resource wise in every case. Pair programming however, might best offer a balanced approach to resourcing and perspective diversity to have more effective communication and knowledge transfer then a single individual can do on their own.

Appendix

Summarizing what we discovered about the perceived benefits of pair programming, two people working together on the same problem derive process improvements that result in better software. The problems perceived with pair programming are all forms of anxiety: individual anxiety working closely with someone else and an organizational anxiety over allocation of funds to pair programming. Many of the attributes of a good pair programming partner are similar to that of a good spouse. The partner should communicate well, complement the other‟s skills and personality, and in some areas be better than the other to stimulate learning experiences as well as help solve problems together. A good pair programming team is fast, efficient, and effective because they have complementary skills, communicate well, are sensitive to the other’s needs and personality, and work without antagonizing one another.

Pair Programming: What’s in it for Me? (2008)

Reviews for quality are hard and time consuming. I personally can’t really review the code looking at the diff, I can give only superficial comments. To understand the code, most of the time I need to fetch it locally and to try to implement the change myself in a different way. To make a meaningful suggestion, I need to implement and run it on my machine (and the first two attempts won’t fly). Hence, a proper review for me takes roughly the same time as the implementation itself. Taking into account the fact that there are many more contributors than maintainers, this is an instant game over for reviews for quality.

Two kinds of code review
Discuss this article

Multiple Dependent Pull Requests in Git

Sukima16th October 2023 at 10:15am

How do I manage multiple dependent Pull Requests in the least chaotic way possible?

Often my work is multi faceted and requires several incremental steps to complete. In efforts to keep the scope of work — and the resultant Pull Requests — to a reasonable size I would want to split the work up. Unfortunately, this introduces a maintenance nightmare as each subsequent branch will quickly diverge as I address feedback on each pull request individually — often catastrophically so.

How can I manage multiple dependent pull requests in a less chaotic way while addressing feedback and maintaining possibly frequent rebasing on the main branch?

This is a complex topic but there is hope. The latest Git versions have several helper options to manage this better. You will need a fairly new version of Git. I’ve tested this with git version 2.41.0.28.

Add the following config options:

git config --global rebase.autosquash true
git config --global rebase.updateRefs true
git config --global rebase.rebaseMerges true

In this example I will build up a PR set. The first PR will be featureA.

* 3d477ad (HEAD -> featureA) A2
* f0751af A1
* 1304eb2 (main) M1

Make your usual Pull Request and base it off main. While that is waiting I'll move on to featureB.

* eb9af14 (HEAD -> featureB) B1
* 3d477ad (HEAD -> featureA) A2
* f0751af A1
* 1304eb2 (main) M1

But while working on featureB I realized I need to fix a bug in another module. The fix is isolated on its own and deserves its own pull request but featureB also depends on the fix. To make it easy I will work on it here and rebase/cherry-pick the changes to their own branch. This will temporarily look like:

* 2fcb9e3 (HEAD -> featureB) B2
* 947f7a7 fix1
* eb9af14 B1
* 3d477ad (featureA) A2
* f0751af A1
* 1304eb2 (main) M1

Then I'll run the following:

# Make a new branch off main
git checkout -b my-fix main # merge base
# Pull in the temp changes from featureB
git cherry-pick 947f7a7
# ... Create pull request ...
git checkout featureB

We will rebase just featureB'on its own. git rebase -i featureA and edit the todo list to remove the no longer needed fix commit(s) and merge the fix branch into this one. Change it from:

label onto

reset onto
pick eb9af14 B1
pick 947f7a7 fix1
pick 2fcb9e3 B2

# Rebase 3d477ad..2fcb9e3 onto 3d477ad (5 commands)

To this:

label onto

reset onto
merge my-fix
pick eb9af14 B1
pick 2fcb9e3 B2

# Rebase 3d477ad..2fcb9e3 onto 3d477ad (5 commands)
* 6843025 (HEAD -> featureB) B2
* 139e024 B1
*   b30d28a Merge branch 'my-fix'
|\
| * e4544ef (my-fix) fix1
* | 3d477ad (featureA) A2
* | f0751af A1
|/
* 1304eb2 (main) M1

Now I'm ready for another pull request. When creating a pull request for featureB make sure to set the base branch in your Pull Request to featureA not main. This is important.

For Bitbucket when you merge a previous PR all subsequent PRs get a chance to be rebased on the new merge. There is a checkbox in the modal dialog for this, make sure you check it.

While working on to featureC we get some feed back on featureA. To keep things easy we address the feed back in featureC but we make the commits as fixup commits so that when we rebase the set Git will apply them in the right place. To do this easily when you commit add the --fixup argument with the SHA of the commit these changes will fix-up.

git commit --fixup f0751af
* 389ccd3 (HEAD -> featureC) fixup! A1
* 76ff438 C1
*   282d2f0 (featureB) Merge branch 'my-fix' into featureB
|\
| * ed6674a (my-fix) fix2
| * d48e7e3 fix1
* | c49ef70 B2
* | fbe9abb B1
* | 9a06f61 (featureA) A2
* | 59358d3 A1
|/
* 08971e5 M1

Time to get our changes up-to-date with main. Do your usual checkout main and pull.

git checkout main
git pull
git checkout -
* 6a9e9e7 (main) M2
| * b7111e3 (HEAD -> featureC) fixup! A1
| * 84f29fe C1
| * 6843025 (featureB) B2
| * 139e024 B1
| *   b30d28a Merge branch 'my-fix'
| |\
| | * e4544ef (my-fix) fix1
| |/
|/|
| * 3d477ad (featureA) A2
| * f0751af A1
|/
* 1304eb2 M1

Then we interactively rebase the entire chain from (in this case) featureC: git rebase -i main which will produce the following todo in your editor:

label onto

# Branch my-fix
reset onto
pick e4544ef fix1
update-ref refs/heads/my-fix

label my-fix

reset onto
pick f0751af A1
fixup b7111e3 fixup! A1
pick 3d477ad A2
update-ref refs/heads/featureA

merge -C b30d28a my-fix # Merge branch 'my-fix'
pick 139e024 B1
pick 6843025 B2
update-ref refs/heads/featureB

pick 84f29fe C1

# Rebase 6a9e9e7..b7111e3 onto 6a9e9e7 (15 commands)

It is recreating the branching structure as well as preserving the merges we’ve done. You may also notice that the fixup was moved to the correct location. Save this off and allow Git to perform all the steps. You’ll get an output that looks like this.

Successfully rebased and updated refs/heads/featureC.
Updated the following refs with --update-refs:
        refs/heads/featureA
        refs/heads/featureB
        refs/heads/my-fix

Some things to note is that your current branch (featureC) is not in the list as a rebase presumes the current branch is the focus of the rebase unlike the others which were side-effects of the rebase. I find it helpful to copy the current branch name and this output for the next step.

These branches are setup locally but we need to push them out to our cloud service for CI and review use. There is no easy way to do this through git but you can write a command to loop over each and push them up. I typically use an editor to do this step. In default Bash you can press CTRL-X+CTRL-E or for Vi bindings ESC+v.

for branch in featureA featureB my-fix featureC; do
  git checkout $branch && git push --force-with-lease
done
* 1be7b9c (HEAD -> featureC) C1
* 783c35b (featureB) B2
* 3120777 B1
*   a9685fe Merge branch 'my-fix'
|\
| * 345b14e (my-fix) fix1
* | 739ca89 (featureA) A2
* | 66f72bf A1
|/
* 6a9e9e7 (main) M2
* 1304eb2 M1

Now my-fix was merged in our cloud service and you did a pull for main. You will need to rebase everything on that. You can git branch -d my-fix since that is no longer needed locally.

*   5c24663 (main) Merge branch 'my-fix'
|\
| | * 1be7b9c (HEAD -> featureC) C1
| | * 783c35b (featureB) B2
| | * 3120777 B1
| | *   a9685fe Merge branch 'my-fix'
| | |\
| | |/
| |/|
| * | 345b14e (my-fix) fix1
|/ /
| * 739ca89 (featureA) A2
| * 66f72bf A1
|/
* 6a9e9e7 M2
* 1304eb2 M1

You may need to update the todo list in this case to remove the merge of my-fix. Also remove the first reset line.

pick 66f72bf A1
pick 739ca89 A2
update-ref refs/heads/featureA

pick 3120777 B1
pick 783c35b B2
update-ref refs/heads/featureB

pick 1be7b9c C1

# Rebase 2e5faef..1be7b9c onto 2e5faef (10 commands)

And again update your cloud service (without the my-fix branch).

for branch in featureA featureB featureC; do
  git checkout $branch && git push --force-with-lease
done
* bfafaef (HEAD -> featureC) C1
* 726034d (featureB) B2
* 4e4d6fb B1
* 4bbc781 (featureA) A2
* 2d55b55 A1
*   2e5faef (main) Merge branch 'my-fix'
|\
| * 345b14e (my-fix) fix1
|/
* 6a9e9e7 M2
* 1304eb2 M1

Finally, featureA is ready to merge. Do so via your cloud service. It should update the subsequent pull requests for you. But once you have, perform another git pull and you can again rebase to update everything.

*   a63253c (main) Merge branch 'featureA'
|\
| | * bfafaef (HEAD -> featureC) C1
| | * 726034d (featureB) B2
| | * 4e4d6fb B1
| |/
| * 4bbc781 (featureA) A2
| * 2d55b55 A1
|/
*   2e5faef Merge branch 'my-fix'
|\
| * 345b14e (my-fix) fix1
|/
* 6a9e9e7 M2
* 1304eb2 M1
git checkout main
git pull
git branch -d featureA
git co -
git rebase -i main
label onto

reset onto
pick 4e4d6fb B1
pick 726034d B2
update-ref refs/heads/featureB

pick bfafaef C1

# Rebase a63253c..bfafaef onto a63253c (6 commands)
* cb76d36 (HEAD -> featureC) C1
* f4ee99c (featureB) B2
* 2d608ce B1
*   a63253c (main) Merge branch 'featureA'
|\
| * 4bbc781 (featureA) A2
| * 2d55b55 A1
|/
*   2e5faef Merge branch 'my-fix'
|\
| * 345b14e (my-fix) fix1
|/
* 6a9e9e7 M2
* 1304eb2 M1

Continue till all pull requests in the chain have been exhausted. I know it is complicated but this is the best way I've figured out how to manage such a complex situation. Not sure how the tech industry arrived at this place but here we are.

Discuss this article

Using XML for a custom DSL for Ember components

Sukima29th September 2023 at 3:52pm

I ran into a interesting situation where I wanted to make an Ember component flexible but the internal use of the copmponent had more complexity that it wasn't easy to make flexible in the usual ways.

Specifically I had a component (I’ll call it FooBar) that was attempting to work like this:

<div ...attributes>
  Dolor commodi quidem earum quidem tenetur ullam. Eveniet
  nihil pariatur sed quis iure
  <ButtonLink @linkTo="…" @analytics="banner-click">
    libero ab porro eum molestiae accusamus.
  </ButtonLink>
  Dolor accusantium ex consectetur rerum suscipit Tempore
  rerum adipisci tempora velit labore molestias et
  similique.
</div>

First draft of the example component template.

What I needed was to be able to control the text from the consumer. The nieve approach would be to paramertarize each part.

<FooBar
  @href="/foobar"
  @analyticsKey="foo-bar"
  @text1="Sit ipsam reiciendis nam eos quo sint hic Vel
    sapiente ex"
  @text2="numquam quos odit qui"
  @text3=". Sapiente laborum aperiam quas natus sunt
    adipisicing Voluptatibus rerum velit perferendis
    nesciunt odit animi Nobis"
/>

Example ussage of the FooBar component using tightly coupled parameters.

I this approach we’ve tightly coupled the parameters with the internal implementation. This will cause difficulties if that format of the implementatgion changes. Perhaps a new requirements wants to add a @text4 or we need to use this component but want to not have a @text2. Changing things like this is difficult.

Instead if we could just offer the FooBar component one parameter that is flexible enough to express the needs any way we wish.

{{! Notice <link>numquam quos odit qui</link> in @text }}
<FooBar
  @href="/foobar"
  @analyticsKey="foo-bar"
  @text="Sit ipsam reiciendis nam eos quo sint hic Vel
    sapiente ex <link>numquam quos odit qui</link>. Sapiente
    laborum aperiam quas natus sunt adipisicing Voluptatibus
    rerum velit perferendis nesciunt odit animi Nobis"
/>

Example usage of FooBar that is loosly coupled to the implementation.

In this example we can change the @text in anyway we need. Opt-in to a link or opt-out. Mix and match as needed.

How can we implement this instead?

<div ...attributes>
  {{#each this.parts as |part|}}
    {{#if part.isLink}}
      <ButtonLink
        @linkTo={{@href}}
        @analytics={{@analyticsKey}}
      >
        {{part.text}}
      </ButtonLink>
    {{else}}
      {{part.text}}
    {{/if}}
  {{/each}}
</div>

FooBar component template

import Component from '@glimmer/component';

interface Signature {
  Args: {
    href: string;
    analyticsKey: string;
    text: string;
  };
}

export default class FooBar extends Component<Signature> {
  get parts() {
    return splitTextIntoParts(this.args.text);
  }
}

function * splitTextIntoParts(text: string) {
  let parser = new DOMParser();
  let dom = parser.parseFromString(
    `<root>${text}</root>`,
    'application/xml'
  );

  for (let node of dom.documentElement.childNodes) {
    if (node instanceof Text) {
      yield {
        isLink: false,
        text: node.data,
      };
    } else if (node instanceof Element) {
      yield {
        isLink: node.tagName === 'link',
        text: node.textContent,
      };
    }
  }
}

FooBar component logic

Discuss this article

Why Ember.js for Enterprise Software Development

Sukima25th September 2023 at 1:38pm

Introduction

Enterprises require robust and reliable software solutions to stay competitive. The choice of a development framework plays a pivotal role in determining the success of these software projects. Among the various options available, Ember.js stands out as a powerful and comprehensive framework that is ideally suited for enterprise-level software development. This essay aims to elucidate why Ember.js is a more appropriate choice than its competitors for developing enterprise software.

I. Strong Conventions

Ember.js is well known for its strong conventions, which provide a clear and structured path for developers to follow. In the enterprise world, where collaboration and scalability are paramount, these conventions offer several benefits. They ensure that team members understand and adhere to best practices consistently, reducing the likelihood of code discrepancies and fostering maintainability. Competing frameworks often lack such rigor, leading to a more chaotic development process and making Ember.js a superior choice for enterprise projects.

II. Stability and Long-Term Support

Enterprise software needs to be reliable and stable, as it forms the backbone of critical business operations. Ember.js is distinguished by its commitment to stability and long-term support. The Ember project follows a strict release cycle with LTS (Long-Term Support) versions, guaranteeing that businesses can rely on a consistent and dependable framework for years. In contrast, many competitors are known for frequent breaking changes and a lack of commitment to backward compatibility, causing headaches for enterprise developers.

III. Extensive Ecosystem

Ember.js boasts a rich ecosystem of add-ons, libraries, and tools, which significantly accelerates the development process. Enterprises can leverage these resources to expedite the creation of complex features, reducing time-to-market. Moreover, Ember's convention over configuration (CoC) approach ensures that many common application level problems have built-in solutions, saving developers from reinventing the wheel. This extensive ecosystem fosters productivity and sets Ember.js apart from competitors that may require extensive custom solutions for similar functionality.

IV. Robust Testing Capabilities

Quality assurance is critical for enterprise software, and Ember.js excels in this aspect. The framework provides a built-in testing suite, methodologies for CI pipelines, and a slew of supporting add-ons (like ember-exam), which all help to streamline the testing process. This ensures that software remains reliable and bug-free even as it evolves. Competing frameworks often lack such integrated testing tools, requiring additional third-party solutions, which can be cumbersome and less effective for enterprise-scale projects.

V. Community

The Ember community has been consistent in their dedication to supporting enterprise-scale solutions. They focus efforts into accessibility, testing, conventions, and support for third-party integrations. Not only are they welcoming to new developers but are also supportive of experienced developers. They hold to inclusive values and provide public transparency to their future plans by means of a defined RFC process. Many of those in the community continue to show a dedication and enthusiasm for helping others.

Conclusion

In the realm of enterprise software development, selecting the right framework is a critical decision that can impact the success of a project and the future of a business. Ember.js emerges as the superior choice among competitors due to its strong conventions, stability, extensive ecosystem, robust testing capabilities, and support for modern web development trends. Its commitment to providing a reliable and efficient platform makes it the optimal choice for enterprises looking to develop scalable, maintainable, and feature-rich software solutions. Ember.js is not just a framework; it is a powerful ally in the pursuit of enterprise excellence.

Discuss this article

Cancelable promises

suki22nd September 2023 at 3:39pm

Cancelable promises have been a topic of debate for a long time since JavaScript first introduced Promises. There hasn’t been a clean way to make a promise cancelable but some have tried.

For example in this article the author explored the idea of allowing an outside signal to abort a timer. They used more promises to accomplish this. However, we now have the AbortController interface which provides a much better way to implement this pattern.

Starting with a non-cancellable example:

function delayedName(delay = 0) {
  return new Promise((resolve) => {
    setTimeout(() => resolve('Sukima'), delay);
  });
}

console.log(await delayedName(1000));

Next we need to support the AbortSignal interface.

function delayedName(delay = 0, signal) {
  let cleanup;

  if (signal.aborted) return Promise.reject(signal.reason);

  return new Promise((resolve, reject) => {
    let abort = ({ target }) => reject(target.reason);
    let timer = setTimeout(() => resolve('Sukima'), delay);

    signal.addEventListener('abort', abort);

    cleanup = () => {
      signal.removeEventListener('abort', abort);
      clearTimeout(timer);
    };
  }).finally(cleanup);
}

delayedName(1000, AbortSignal.timeout(500))
  .then((name) => console.log(name))
  .catch((error) => console.log(error));

The pro of this approach is that we leave the construction and management of the signal to the consumer a single signal can be scaled over more than one use.

When attaching an event to a singnal that is managed elsewhere be sure to remove the handler or it may become a memory leak.

An alernative might be to generate your own controller. With this you don’t need to worry about removing the handler because the scope is tightly coupled to the invocation. By the time we would need to cleanup the parent scope will be invalid. And the garbage collection will take care of it.

Note

That sentence reads weird but it made sense in my head at the time.

What that was suposed to mean was if your function creates a resource and that resource becomes marked for disposing it likely doesn't need cleanup (or if it did has an interface for it).

But if your function takes an external resource and subscribes to that resource than the scope of the resource is out of your function’s control. And you need to be sure you have a way to unsubscribe or you risk a possible memory leak.

’Nuff said, here is an alternative interface:

function delayedName(delay = 0) {
  let timer;
  let controller = new AbortController();
  return {
    abort: (reason) => controller.abort(reason),
    promise: new Promise((resolve, reject) => {
      timer = setTimeout(() => resolve('Sukima'), delay);
      controller.signal.onabort =
        ({ target }) => reject(target.reason);
    }).finally(() => clearTimeout(timer)),
  };
}

let { abort, promise } = delayedName(1000);
promise.then(
  (name) => console.log(name),
  (error) => console.log(error),
);
setTimeout(abort, 500);

The signal pattern here is pretty neat as it offers a lot of flexibility in how it is used while also keeping good encapsulation and separation of concerns.

A controller is responsible for providing a signal instance and a way to set the state of the signal. The signal is only responsible for providing its state and managing event subscriptions. This way the signal instance can be passed around without exposing the ability to mutate its state. While the original controller can still effect that signal’s state ― even from a distance.

The AbortController is tightly coupled to the concept of aborting in this case. Thus, you can make such a pattern yourself in cases where you had a different concept to express.

class Signal() {
  #controller;

  constructor(controller) {
    this.#controller = controller;
  }

  get triggered() {
    return this.#controller.triggered;
  }

  subscribe(listener) {
    if (this.triggered) return;
    this.#controller.listeners.add(listener);
  }

  unsubscribe(listener) {
    if (this.triggered) return;
    this.#controller.listeners.delete(listener);
  }

  static trigger() {
    let controller = new SignalController();
    controller.trigger();
    return controller.signal;
  }

  static timeout(delay) {
    let controller = new SignalController();
    setTimeout(() => controller.trigger(), delay);
    return controller.signal;
  }

  static any(signals) {
    let controller = new SignalController();
    let subscription = () => controller.trigger();
    for (let signal of signals)
      signal.subscribe(subscription);
    return controller.signal;
  }
}

class SignalController {
  #triggered = false;
  #listeners = new Set();
  #signal = new Signal(this);

  get signal() {
    return this.#signal;
  }

  trigger() {
    this.#triggered = true;
    this.#listeners.forEach((i) => i());
    this.#listeners.clear();
  }
}

let signal = Signal.timeout(1000);
signal.subscribe(() => console.log('triggered'));

If anyone is curious the HTML spec suggests a static method any() which is useful to compose many signals into one. Unfortunatly, it seems like this hasn’t been implemented in JS engines yet. Thus, here is an implementation to copy-pasta:

Discuss this article

Modal dialog wizards

Sukima8th August 2023 at 5:08am

A while back I wrote about Modal dialogs and their use of a ModalManager to help make the complexities of managing modal dialogs easier and interface with JavaScript async/await semantics. In this article I wanted to expand on that idea in presenting a series of dialogs that combined follow the Wizard design pattern.

For a quick recap, the ModalManager is a class that knows how to open a modal dialog, react to events to close it, and resolves a promise with one of three reasons: confirmed, rejected, and cancelled. In a typical confirmation dialog confirmed and rejected corresponding to a yes/no question; cancelled corresponding to the ✗ button or other UI dismissal.

With these logic flows we can correlate these to the paths each wizard step would take.

Relationships between ModalManager reasons and Wizard actions
Manager reasonWizard action
confirmedNext button
rejectedPrevious button
cancelledDialog cancelled (✗ button)

What this looks like is we establish a <dialog> for each wizard step. Use a ModalManager to open each step one at a time. As each step is dismissed we track the progress through the wizard via a state machine. And finally resolve a promise to the full result of all the steps combined.

View the full demo

Our fist step is to define the dialogs. Each dialog element will have a data-state value to correspond to a state which we can use for a little bit of meta-programming and make our ModalWizard class more decoupled.

HTML of step two dialog
<dialog data-state="stepTwo">
  <div>
    <header>
      <h1>Wizard step two</h1>
      <button
        type="button"
        data-action="cancel"
        aria-label="Cancel wizard"
      >&cross;</button>
    </header>

    <article>
      <p>What is your favorite color?</p>
      <p>
        <label for="color">Favorite color:</label>
        <input
          id="color"
          name="Favorite color"
          form="example-form"
        >
      </p>
    </article>

    <footer>
      <button type="button" data-action="reject">
        Back
      </button>
      <button type="button" data-action="confirm">
        Next
      </button>
    </footer>
  </div>
</dialog>

Usage

async function eventHanlder() {
  let result = await new ModalWizard().start();
  if (result.isCancelled) return;
  // Do something with result
}

The idea here is that the ModalWizard initializes an internal state. The .start() begins the wizard flow and returns a promise. This promise resolves to a result object.

The result object might contain getters that represent the final state and possibly any other needed data.

In the demo above I used a single <form> to collect data from each <dialog> which made the demo easier to read and allowed it to focus on the ModalWizard more than the implementation details of the demo.

ModalWizard

The implementation will hold a state and provide an .start() method.

class ModalWizard {
  state = 'inactive';

  get result() {
    return new WizardResult(this.state);
  }

  async start() {
    this.state = this.transition('start');
    for await (let { reason } of this.modals())
      this.state = this.transition(reason);
    return this.result;
  }
}

Example WizardResult class
class WizardResult {
  constructor(state) {
    this.state = state;
  }

  get isDone() {
    return this.state === 'done';
  }

  get isCancelled() {
    return this.state === 'cancelled';
  }

  get isFinished() {
    return this.isDone || this.isCancelled;
  }

  get isRunning() {
    return !(this.isFinished || this.state === 'inactive');
  }
}

If you notice the .start() transitions the state machine with a start event. Then it loops over a generator called modals() which based on the state will yield the promise provided by ModalManager.open() for that specific state.

In short, we’ve separated the concerns into three parts.

  1. a loop that requests the next modal in the flow and adjusts the state according to the modal’s result
  2. an async iterator which provides a modal result specific for the current state
  3. a transition method that determines the next state based on the event/previous modal result
class ModalWizard {
  …
  async *modals() {
    while (true) {
      // When the state machine is done stop the iteration
      if (this.result.isFinished) return;

      // Find the dialog for this state
      let dialog = document.querySelector(
        `dialog[data-state=${this.state}]`
      );

      // Protect from a missing dialog which is
      // a programming error but without the exception would
      // induce an infinite loop
      if (!dialog) throw new Error(
        `Missing dialog for state ${this.state}`
      );

      // Use the ModalManager and open the dialog yielding
      // the promise so its fulfillment can be awaited in
      // the start() method
      yield new ModalManager(dialog).open();
    }
  }
}

In the transition() example I used a reducer pattern as the state machine. I think that if the logic of the wizard became more complex ― perhaps with branching steps ― I would turn to something like XState instead of a reducer function.

class ModalWizard {
  …
  transition(event) {
    // Handle any universal events
    // These events will always result in a new state
    switch (event) {
      case 'cancelled': return 'cancelled';
      case 'start': return 'stepOne';
      // No default as we want to fall through to the next
      // switch statement
    }

    // These are state changes based on both the current
    // state and the event.
    //
    // It goes state first to lower the amount of code to
    // change as states are added and removed. States change
    // more often than events. This is a trade off choice
    // from maintenance experience.
    //
    // All state machines when recived an unknown or
    // unhandled event should result in the same state as it
    // is currently in.
    switch (this.state) {
      case 'stepOne':
        switch (event) {
          case 'confirmed': return 'stepTwo'; // next
          case 'rejected': return 'cancelled'; // back
          default: return this.state;
        }
      case 'stepTwo':
        switch (event) {
          case 'confirmed': return 'stepOne'; // next
          case 'rejected': return 'stepThree'; // back
          default: return this.state;
        }
      case 'stepThree':
        switch (event) {
          case 'confirmed': return 'stepTwo'; // next
          case 'rejected': return 'done'; // back
          default: return this.state;
        }
      default: return this.state;
    }
  }
}

My hope is that I’ve been able to demonstrate some alternative ways to leverage the JavaScrtipt language itself to accomplish a fairly complex idea like multiple modal dialogs working together in a wizard style UI.

I’ve shown how async genreators can be used to provide promises from derived state. How for await…of can be used to manage a wait then react cycle. And how to funnel the decision making logic into a state machine ― in this case a reducer function.

And finally, I hope I’ve manged to demonstrate how separating concerns between specialized and generalized classes can benefit both understanding and maintainability.

Discuss this article