Skip to content

Add a small utility for tracking (and reacting to) repeated events

What does this MR do?

For #292008 (closed)

Adds a small utility that tracks occurrences of anything (as determined by the call-site logic) and reacts to particular counts with caller-defined behavior.

Why is this so large? 😠

Sorry!

It's the tests and JSDoc blocks. The actual code here is about 50 LoC.

Sorry, such is the life we live!

Here's all of the code (without tests) with the comments removed...

It's certainly not as easy to review, but this comment-free version might be easier to scan initially and get a good grasp of what's happening.

import { uuids } from '../../diffs/utils/uuids';

const instances = {};

export function create() {
  const id = uuids()[0];
  let handlers = {};
  let count = 0;

  const instance = {
    get id() {
      return id;
    },
    get count() {
      return count;
    },
    get handlers() {
      return handlers;
    },

    free: () => {
      return delete instances[id];
    },
    handle: (onCount, behavior) => {
      if (onCount && behavior) {
        handlers[onCount] = behavior;
      }
    },
    eject: (onCount) => {
      if (onCount) {
        delete handlers[onCount];
      }
    },
    occur: () => {
      count += 1;

      if (typeof handlers[count] === 'function') {
        handlers[count](count);
      }
    },
    reset: ({ currentCount = true, handlersList = false } = {}) => {
      if (currentCount) {
        count = 0;
      }

      if (handlersList) {
        handlers = {};
      }
    },
  };

  instances[id] = instance;

  return instance;
}

export function recall(id) {
  return instances[id];
}

export function free(id) {
  return recall(id)?.free() || false;
}

Fine, what does this do?

It tracks occurrences of something when you expect it to happen multiple times. Thus recurrence.

Here's a very rough example:

let count = 0;

// something occurs

count += 1;

if( count == 5 ){
    // do things
}
else if( count == 10 ){
    // do another thing
}

// it occurs again

count += 1;

if( count == 5 ){
    // do things
}
else if( count == 10 ){
    // do another thing
}

This is annoying.

A better implementation of the above...
let count = 0;
const itHappened = () => {
    count += 1;

    if( count == 5 ){
        // do things
    }
    else if( count == 10 ){
        // do another thing
    }
};
 
// something occurs

itHappened();

// it occurs again

itHappened()

As you can see, this is starting to look a lot like the abstracted implementation found in this MR...


Here's 👇🏻 what this utility makes that 👆🏻 :

import { create } from 'lib/utils/recurrence';

const thingRecurrence = create();

thingRecurrence.handle( 5, () => { // do things } );
thingRecurrence.handle( 10, () => { // do another thing } );

// something occurs
thingRecurrence.occur();
// it occurs again
thingRecurrence.occur();

Not sure I see why...

Check out !60533 (merged) for the ur-example.

tl;dr: We need to track network errors across multiple calls. But - in practice - those calls aren't actually linked: they're not retries of the same initial call, for example. So this helper will count occurrences of a network call failing, and trigger error displays only when it's not a transient failure (2 in a row).

As you can imagine from the boilerplate above, doing this manually in the action is a bit klunky. Also, storing it in Vuex is extremely inefficient: we don't need a global object that informs every single subscriber, we just need a closure over some data!

Okay, but...

It's

  • Fully documented with JSDoc blocks
    • Make sure you're up to date (yarn upgrade) and run yarn run jsdoc. Once it's compiled, serve the /jsdocs folder (I like npx serve) and check out the extensive documentation!
  • Pretty robust internally
    • There are plenty of safety checks when functions try to execute behavior that guard against unexpected use paths.
    • It uses getters to expose values that shouldn't be mutated externally
  • Tested thoroughly
    • There are almost as many individual tests as there are lines of functional code in the source file!

Where are we?

MR Description
We're here 👉🏻 Add a small utility that adds a stateful memory bank for tracking repeated occurrences of [thing]. This is a small, generic utility that's not specific to error handling, it just remembers how many times you tell it something happened, and it will perform your requested behaviors for certain counts.
!60537 (merged) Make Flash messages programmatically dismissable
!60533 (merged) When the Notes app is polling for new notes and that request fails, wait to display the error until we're sure the failure isn't transient (it happens twice in a row)

Screenshots (strongly suggested)

N/A, this is all utility JavaScript code.

Does this MR meet the acceptance criteria?

Conformity

Availability and Testing

Security

If this MR contains changes to processing or storing of credentials or tokens, authorization and authentication methods and other items described in the security review guidelines:

  • [-] Label as security and @ mention @gitlab-com/gl-security/appsec
  • [-] The MR includes necessary changes to maintain consistency between UI, API, email, or other methods
  • [-] Security reports checked/validated by a reviewer from the AppSec team
Edited by Thomas Randolph

Merge request reports