2
\$\begingroup\$

I have this class EfficientStorageEventProvider, which is responsible for efficient handling of StorageEvent

EfficientStorageEventProvider.ts

import { IStorageEventRecord } from "./IStorageEventRecord";
import { defer, Observable, share, Subject } from "rxjs";
import { doOnSubscriberCountChange } from "rx-do-on-subscriber-count-change";
import { isDefined, isUndefined } from "is-null";

/**
 * {@link EfficientStorageEventProvider} is responsible for:
 * - Efficient `"storage"` event handling
 * (1 check if key is in map of observed keys per `StorageEvent`,
 * instead of having `key_count` checks in every event listener
 * if we should handle event with this key in current handler)
 */
export class EfficientStorageEventProvider {
  private _observedKeyCount = 0;

  private readonly _observedKeyToStorageEventRecordMap = new Map<
    string,
    IStorageEventRecord
  >();

  private readonly _handleStorageEvent: (storageEvent: StorageEvent) => void;

  constructor() {
    this._handleStorageEvent = (storageEvent: StorageEvent) => {
      if (storageEvent.key !== null) {
        const storageEventRecord = this._observedKeyToStorageEventRecordMap.get(
          storageEvent.key,
        );

        if (isDefined(storageEventRecord)) {
          storageEventRecord.storageEventS.next(storageEvent);
        }
      } else {
        // If `key` is `null`, user requested to delete all items
        this._observedKeyToStorageEventRecordMap.forEach(
          (storageEventRecord2) => {
            storageEventRecord2.storageEventS.next(storageEvent);
          },
        );
      }
    };
  }

  /**
   * Notice that order of events is not guaranteed,
   * and following from it, not all events may be processed at all!,
   * because see https://stackoverflow.com/questions/77969202/in-which-order-storage-event-is-received
   */
  getStorageEventForKey$(key: string): Observable<StorageEvent> {
    return defer(() => {
      let storageEventRecord =
        this._observedKeyToStorageEventRecordMap.get(key);

      if (isUndefined(storageEventRecord)) {
        const storageEventS = new Subject<StorageEvent>();
        const storageEvent$ = storageEventS.pipe(
          doOnSubscriberCountChange((newSubscriberCount) => {
            if (newSubscriberCount === 1) {
              this._tryStartListeningStorageEvent();
            } else if (newSubscriberCount === 0) {
              this._observedKeyToStorageEventRecordMap.delete(key);
              this._tryStopListeningStorageEvent();
            }
          }),
          share(),
        );

        storageEventRecord = {
          storageEvent$,
          storageEventS,
        };

        this._observedKeyToStorageEventRecordMap.set(key, storageEventRecord);
      }

      return storageEventRecord.storageEvent$;
    });
  }

  private _tryStartListeningStorageEvent() {
    if (++this._observedKeyCount === 1) {
      window.addEventListener("storage", this._handleStorageEvent);
    }
  }

  private _tryStopListeningStorageEvent() {
    if (--this._observedKeyCount === 0) {
      window.removeEventListener("storage", this._handleStorageEvent);
    }
  }
}

doOnSubscriberCountChange.ts

import { Observable, OperatorFunction, Subscriber } from "rxjs";

export function doOnSubscriberCountChange<TValue>(
  handleNewSubscriberCount: (newSubscriberCount: number) => void,
): OperatorFunction<TValue, TValue> {
  return (source$) => {
    let subscriberCount = 0;

    return new Observable((subscriber: Subscriber<TValue>) => {
      const subscription = source$.subscribe(subscriber);
      handleNewSubscriberCount(++subscriberCount);

      return () => {
        subscription.unsubscribe();
        handleNewSubscriberCount(--subscriberCount);
      };
    });
  };
}

IStorageEventRecord.ts

import { Observable, Subject } from "rxjs";

export interface IStorageEventRecord {
  storageEvent$: Observable<StorageEvent>;
  storageEventS: Subject<StorageEvent>;
}

How can we improve it from point of view of performance/correctness/helpful-comments/more-representative-names?

\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

rxjs has a steep learning curve, and it is difficult to explain to people how this code works or why it is correct.

Removing rxjs significantly eased understanding of this code, even by newer members of our team.

import { isDefined, isUndefined } from "is-null";

/**
 * {@link EfficientStorageEventHandler} is responsible for:
 * - Efficient `"storage"` event handling
 * (1 check if key is in map of observed keys per `StorageEvent`
 * and then calling correct handlers for key,
 * instead of having `key_count` checks in every event listener
 * if we should handle event with this key in current handler)
 */
export class EfficientStorageEventHandler {
  private readonly _handleStorageEvent: (storageEvent: StorageEvent) => void;

  private _observedKeyCount = 0;

  private readonly _observedKeyToStorageEventHandlerSetMap = new Map<
    string,
    Set<(storageEvent: StorageEvent) => void>
  >();

  constructor() {
    this._handleStorageEvent = (storageEvent: StorageEvent) => {
      if (storageEvent.key !== null) {
        const storageEventHandlerSet =
          this._observedKeyToStorageEventHandlerSetMap.get(storageEvent.key);

        if (isDefined(storageEventHandlerSet)) {
          for (const storageEventHandler of storageEventHandlerSet) {
            storageEventHandler(storageEvent);
          }
        }
      } else {
        // If `key` is `null`, user requested to delete all items
        this._observedKeyToStorageEventHandlerSetMap.forEach(
          (storageEventHandlerSet2) => {
            for (const storageEventHandler of storageEventHandlerSet2) {
              storageEventHandler(storageEvent);
            }
          },
        );
      }
    };
  }

  /**
   * Notice that order of events is not guaranteed,
   * and following from it, not all events may be processed at all!,
   * because see https://stackoverflow.com/questions/77969202/in-which-order-storage-event-is-received
   *
   * @return function which will stop listening `storageEventHandler`
   * when called
   */
  addStorageEventHandler(
    key: string,
    storageEventHandler: (storageEvent: StorageEvent) => void,
  ): () => void {
    let storageEventHandlerSet =
      this._observedKeyToStorageEventHandlerSetMap.get(key);

    if (isUndefined(storageEventHandlerSet)) {
      this._tryStartListeningStorageEvent();

      storageEventHandlerSet = new Set<(storageEvent: StorageEvent) => void>([
        storageEventHandler,
      ]);

      this._observedKeyToStorageEventHandlerSetMap.set(
        key,
        storageEventHandlerSet,
      );
    } else {
      storageEventHandlerSet.add(storageEventHandler);
    }

    // Making `storageEventHandlerSet2` `const` to tell compiler that
    // `storageEventHandlerSet2` can't change and is always defined
    const storageEventHandlerSet2 = storageEventHandlerSet;

    return () => {
      storageEventHandlerSet2.delete(storageEventHandler);

      if (storageEventHandlerSet2.size === 0) {
        this._observedKeyToStorageEventHandlerSetMap.delete(key);
        this._tryStopListeningStorageEvent();
      }
    };
  }

  private _tryStartListeningStorageEvent() {
    if (++this._observedKeyCount === 1) {
      window.addEventListener("storage", this._handleStorageEvent);
    }
  }

  private _tryStopListeningStorageEvent() {
    if (--this._observedKeyCount === 0) {
      window.removeEventListener("storage", this._handleStorageEvent);
    }
  }
}

If we need to combine it with rxjs, we can use code like

const efficientStorageEventHandler = new EfficientStorageEventHandler();
const key = 'someKey'
const efficientStorageEvent$ = new Observable<StorageEvent>((subscriber) => {
  const unsubscribe = efficientStorageEventHandler.addStorageEventHandler(
    key,
    (storageEvent) => {
      subscriber.next(storageEvent);
    },
  );

  return unsubscribe;
});
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.