Skip to content

Instantly share code, notes, and snippets.

@andreineculau
Last active October 7, 2019 20:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andreineculau/d892eeb40ac44d6d8446c659b130295e to your computer and use it in GitHub Desktop.
Save andreineculau/d892eeb40ac44d6d8446c659b130295e to your computer and use it in GitHub Desktop.
original, simpler, proper --- typing isn't everything
// https://github.com/m59peacemaker/browser-insular-observer
//
// Usage:
// let observe = InsularObserver(MutationObserver);
// observe(document.documentElement, options, function(documentElementEntry) {...});
//
import keyMaster = require('key-master')
interface Observer<Target, Options> {
observe(target: Target, options?: Options): void
unobserve?(target: Target): void
}
interface ObserverEntry<Target> {
target: Target
}
type ObserverCallback<Target, Entry extends ObserverEntry<Target>> = (entries: Entry[]) => void
type ObserverClass<Target, Options, Entry extends ObserverEntry<Target>> = (
new(callback: ObserverCallback<Target, Entry>, options?: Options) => Observer<Target, Options>
)
type TargetListener<Target, Entry extends ObserverEntry<Target>> = (entry: Entry) => void
interface ObserveCallback<
Target,
Options,
Entry extends ObserverEntry<Target>,
Listener extends TargetListener<Target, Entry>,
> {
(target: Target, listener: Listener): UnobserveCallback
(target: Target, options: Options, listener: Listener): UnobserveCallback
}
type UnobserveCallback = () => void
const InsularObserver = <
Target extends object,
Options,
Entry extends ObserverEntry<Target>,
Listener extends TargetListener<Target, Entry>,
>(
ObserverConstructor: ObserverClass<Target, Options, Entry>,
options?: Options,
): ObserveCallback<Target, Options, Entry, Listener> => {
const listeners = keyMaster<Target, Listener[]>(() => [], new WeakMap<Target, Listener[]>())
const callback = (entries: Entry[]) => {
entries.forEach(entry => {
const targetListeners = listeners.get(entry.target)
targetListeners.forEach(listener => listener(entry))
})
}
const observer = new ObserverConstructor(callback, options)
function observe (
target: Target,
observeOptions: undefined | Options | Listener,
listener?: Listener,
): UnobserveCallback {
if (typeof observeOptions === 'function') {
listener = observeOptions as Listener
observeOptions = undefined
}
const targetListeners = listeners.get(target)
targetListeners.push(listener as Listener)
observer.observe(target, observeOptions)
return function unobserve (): void {
const idx = targetListeners.indexOf(listener as Listener)
targetListeners.splice(idx , 1)
if (targetListeners.length === 0) {
listeners.delete(target)
return observer.unobserve && observer.unobserve(target)
}
}
}
return observe
}
export = InsularObserver
// 1.original.ts transpiled via https://www.typescriptlang.org/play
import keyMaster = require('key-master');
export const InsularObserver = (ObserverConstructor, options) => {
const listeners = keyMaster(() => [], new WeakMap());
const callback = (entries) => {
entries.forEach(entry => {
const targetListeners = listeners.get(entry.target);
targetListeners.forEach(listener => listener(entry));
});
};
const observer = new ObserverConstructor(callback, options);
function observe(target, observeOptions, listener) {
if (typeof observeOptions === 'function') {
listener = observeOptions;
observeOptions = undefined;
}
const targetListeners = listeners.get(target);
targetListeners.push(listener);
observer.observe(target, observeOptions);
return function unobserve() {
const idx = targetListeners.indexOf(listener);
targetListeners.splice(idx, 1);
if (targetListeners.length === 0) {
listeners.delete(target);
return observer.unobserve && observer.unobserve(target);
}
};
}
return observe;
};
// rewritten
// uses _.memoize - makes no assumption on the type of entries
// uses _.isMatch - makes no assumption on the type of entries, nor observe() signature
// no assumption on observe() signature, neither argument count, nor argument type
// no assumption over constructor signature (create a class with hardcoded constructor arguments, if needed)
//
// Usage:
// let observer = reuseObserver(MutationObserver);
// canonical form:
// observer.observe({element: document.documentElement}, function(documentElementEntries) {...}, document.documentElement);
// or convenience form:
// observer.observe(document.documentElement, function(documentElementEntries) {...});
//
// let observer = reuseObserver(IntervalObserver);
// observer.observe({interval: 1000}, function(everySecondEntries) {...}, {interval: 1000});
//
import _ from 'lodash';
let _observerIsElementObserver = function(observer) {
if (observer instanceof IntersectionObserver) {
return true;
}
if (observer instanceof MutationObserver) {
return true;
}
if (observer instanceof ResizeObserver) {
return true;
}
return false;
};
export let reuseObserver = function(Observer, isEntryMatch = _.isMatch) {
let matchListenersPairs = []; // [{match, listeners}]
let cb = function(entries) {
_.forEach(matchListenersPairs, function({match, listeners}) {
let matchingEntries = _.filter(entries, function(entry) {
return isEntryMatch(entry, match);
});
_.forEach(listeners, function(listener) {
listener(matchingEntries);
});
});
};
let observer = new Observer(cb);
let originalObserve = observer.observe.bind(observer);
observer.observe = function(match, listener, ...observeArgs) {
// convenience for known element observers
if (_observerIsElementObserver(observer)) {
if (_.isElement(match)) {
match = {
target: match
};
}
if (!_.isElement(observeArgs[0])) {
observeArgs.unshift(match.target);
}
}
let matchListenersPair = _.find(matchListenersPairs, {
match
});
if (_.isUndefined(matchListenersPair)) {
matchListenersPair = {
match,
listeners: []
};
matchListenersPairs.push(matchListenersPair);
}
matchListenersPair.listener.push(listener);
originalObserve(...observeArgs);
};
return observe;
};
reuseObserver = _.memoize(reuseObserver, function(Observer, isEntryMatch) {
return Observer.toString() + isEntryMatch.toString();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment