Skip to content

Instantly share code, notes, and snippets.

@fabiospampinato
Created February 28, 2022 08:29
Show Gist options
  • Save fabiospampinato/4fc3eeeef21eae004f8f53413dda6ec0 to your computer and use it in GitHub Desktop.
Save fabiospampinato/4fc3eeeef21eae004f8f53413dda6ec0 to your computer and use it in GitHub Desktop.
// src/batch.ts
var Batch = class {
constructor() {
this.level = 0;
this.registerUpdate = (observable2, value) => {
if (!this.queue)
return;
this.queue.set(observable2, value);
};
this.wrap = (fn) => {
const queuePrev = this.queue;
const queueNext = queuePrev || /* @__PURE__ */ new Map();
this.level += 1;
this.queue = queueNext;
try {
fn();
} finally {
this.level -= 1;
this.queue = queuePrev;
if (!this.level) {
this.flush(queueNext);
}
}
};
this.flush = (queue) => {
queue.forEach((value, observable2) => observable2.set(value));
};
this.has = () => {
return !!this.queue;
};
}
};
var batch_default = new Batch();
// src/constants.ts
var NOOP = () => {
};
var SYMBOL = Symbol("Observable");
// src/callable.ts
var callable = (() => {
const self = function() {
return this;
};
const traps = {
get(target, property) {
if (property === SYMBOL)
return true;
const observable2 = target();
return observable2[property].bind(observable2);
},
apply(target, thisArg, args) {
if (!args.length)
return target().get();
return target().set(args[0]);
}
};
return (observable2) => {
return new Proxy(self.bind(observable2), traps);
};
})();
var callable_default = callable;
// src/utils.ts
var cloneDeep = (value) => {
return JSON.parse(JSON.stringify(value));
};
var { isArray } = Array;
var isFunction = (value) => {
return typeof value === "function";
};
var isPrimitive = (value) => {
if (value === null)
return true;
const type = typeof value;
return type !== "object" && type !== "function";
};
var isSet = (value) => {
return value instanceof Set;
};
var isUndefined = (value) => {
return value === void 0;
};
// src/observer.ts
var Observer = class {
registerCleanup(cleanup) {
if (!this.cleanups) {
this.cleanups = cleanup;
} else if (isArray(this.cleanups)) {
this.cleanups.push(cleanup);
} else {
this.cleanups = [this.cleanups, cleanup];
}
}
registerError(error) {
if (!this.errors) {
this.errors = error;
} else if (isArray(this.errors)) {
this.errors.push(error);
} else {
this.errors = [this.errors, error];
}
}
registerObservable(observable2) {
if (!this.observables) {
this.observables = observable2;
} else if (isArray(this.observables)) {
this.observables.push(observable2);
} else {
this.observables = [this.observables, observable2];
}
}
registerObserver(observer) {
if (!this.observers) {
this.observers = observer;
} else if (isArray(this.observers)) {
this.observers.push(observer);
} else {
this.observers = [this.observers, observer];
}
}
registerParent(observer) {
this.parent = observer;
}
registerSelf() {
if (!this.observables) {
return;
} else if (isArray(this.observables)) {
context_default.registerObservables(this.observables);
} else {
context_default.registerObservable(this.observables);
}
}
unregisterObserver(observer) {
if (!this.observers) {
return;
} else if (isArray(this.observers)) {
const index = this.observers.indexOf(observer);
if (index >= 0) {
this.observers.splice(index, 1);
}
} else {
if (this.observers === observer) {
delete this.observers;
}
}
}
unregisterParent() {
delete this.parent;
}
update() {
delete this.dirty;
}
updateError(error, silent) {
const { errors, parent } = this;
if (errors) {
if (isArray(errors)) {
errors.forEach((fn) => fn(error));
} else {
errors(error);
}
return true;
} else {
if (parent) {
if (parent.updateError(error, true))
return true;
}
if (!silent) {
throw error;
}
return false;
}
}
static unsubscribe(observer) {
const { observers, observables, cleanups, errors } = observer;
if (observers) {
if (isArray(observers)) {
for (let i = 0, l = observers.length; i < l; i++) {
Observer.unsubscribe(observers[i]);
}
observers.length = 0;
} else {
Observer.unsubscribe(observers);
delete observer.observers;
}
}
if (observables) {
if (isArray(observables)) {
for (let i = 0, l = observables.length; i < l; i++) {
observables[i].unregisterObserver(observer);
}
observables.length = 0;
} else {
observables.unregisterObserver(observer);
delete observer.observables;
}
}
if (cleanups) {
if (isArray(cleanups)) {
for (let i = 0, l = cleanups.length; i < l; i++) {
cleanups[i]();
}
cleanups.length = 0;
} else {
cleanups();
delete observer.cleanups;
}
}
if (errors) {
if (isArray(errors)) {
errors.length = 0;
} else {
delete observer.errors;
}
}
}
};
var observer_default = Observer;
// src/context.ts
var Context = class {
constructor() {
this.sampling = false;
this.registerCleanup = (cleanup) => {
if (!this.observer)
return;
this.observer.registerCleanup(cleanup);
};
this.registerError = (error) => {
if (!this.observer)
return;
this.observer.registerError(error);
};
this.registerObservable = (observable2) => {
if (!this.observer)
return;
if (this.sampling)
return;
if (observable2.hasObserver(this.observer))
return;
this.observer.registerObservable(observable2);
observable2.registerObserver(this.observer);
};
this.registerObservables = (observables) => {
if (!this.observer)
return;
if (this.sampling)
return;
observables.forEach(this.registerObservable);
};
this.registerObserver = (observer) => {
if (!this.observer)
return;
this.observer.registerObserver(observer);
observer.registerParent(this.observer);
};
this.unregisterObserver = (observer) => {
if (!this.observer)
return;
this.observer.unregisterObserver(observer);
observer.unregisterParent();
};
this.wrap = (fn) => {
const observer = new observer_default();
try {
return this.wrapWith(fn, observer, true);
} catch (error) {
observer.updateError(error);
}
};
this.wrapVoid = (fn) => {
this.wrap(fn);
};
this.wrapWith = (fn, observer, disposable, sampling) => {
const observerPrev = this.observer;
const samplingPrev = this.sampling;
this.observer = observer;
this.sampling = !!sampling;
try {
const dispose = observer && disposable ? () => this.dispose(observer) : NOOP;
return fn(dispose);
} finally {
this.observer = observerPrev;
this.sampling = samplingPrev;
}
};
this.wrapWithout = (fn) => {
return this.wrapWith(fn);
};
this.wrapWithSampling = (fn) => {
return this.wrapWith(fn, this.observer, false, true);
};
this.dispose = (observer) => {
observer_default.unsubscribe(observer);
this.observer = void 0;
};
}
};
var context_default = new Context();
// src/observable.ts
var Observable = class {
constructor(value, options, parent) {
this.value = value;
if (options) {
if (options.comparator) {
this.comparator = options.comparator;
}
}
if (parent) {
this.parent = parent;
}
}
hasObserver(observer) {
if (!this.observers) {
return false;
} else if (isSet(this.observers)) {
return this.observers.has(observer);
} else {
return this.observers === observer;
}
}
registerObserver(observer) {
if (!this.observers) {
this.observers = observer;
} else if (isSet(this.observers)) {
this.observers.add(observer);
} else if (this.observers === observer) {
return;
} else {
this.observers = /* @__PURE__ */ new Set([this.observers, observer]);
}
}
unregisterObserver(observer) {
if (!this.observers) {
return;
} else if (isSet(this.observers)) {
this.observers.delete(observer);
} else if (this.observers === observer) {
this.observers = void 0;
}
}
registerSelf() {
if (this.parent) {
if (!this.parent.dirty) {
this.parent.registerSelf();
} else {
this.parent.update();
}
} else {
context_default.registerObservable(this);
}
}
get() {
this.registerSelf();
return this.value;
}
sample() {
return this.value;
}
set(value) {
if (Observable.compare(value, this.value, this.comparator)) {
return this.value;
}
if (batch_default.has()) {
batch_default.registerUpdate(this, value);
return value;
} else {
this.value = value;
this.emit();
return value;
}
}
produce(fn) {
const isValuePrimitive = isPrimitive(this.value);
const valueClone = isValuePrimitive ? this.value : cloneDeep(this.value);
const valueResult = fn(valueClone);
const valueNext = isValuePrimitive || !isUndefined(valueResult) ? valueResult : valueClone;
return this.set(valueNext);
}
update(fn) {
const valueNext = fn(this.value);
return this.set(valueNext);
}
emit() {
const { observers } = this;
if (!observers)
return;
if (isSet(observers) && !observers.size)
return;
context_default.wrapWithout(() => {
if (isSet(observers)) {
const queue = Array.from(observers.values());
for (let i = 0, l = queue.length; i < l; i++) {
const observer = queue[i];
observer.dirty = true;
}
for (let i = 0, l = queue.length; i < l; i++) {
const observer = queue[i];
if (!observer.dirty)
continue;
if (!observers.has(observer))
continue;
observer.update();
}
} else {
observers.update();
}
});
}
on(fn, options, dependencies) {
if (isArray(options))
return this.on(fn, void 0, options);
const observable2 = computed_default.wrap(() => {
this.get();
if (dependencies)
dependencies.forEach((observable3) => observable3());
return context_default.wrapWithSampling(() => fn(this.value));
}, void 0, options);
return observable2;
}
static compare(value, valuePrev, comparator = Object.is) {
return comparator(value, valuePrev);
}
};
var observable_default = Observable;
// src/computed.ts
var Computed = class extends observer_default {
constructor(fn, valueInitial, options) {
super();
this.fn = fn;
this.observable = new observable_default(valueInitial, options, this);
this.update();
}
update() {
context_default.registerObserver(this);
observer_default.unsubscribe(this);
delete this.dirty;
const valuePrev = this.observable.sample();
try {
const valueNext = context_default.wrapWith(() => this.fn(valuePrev), this, true);
this.observable.set(valueNext);
} catch (error) {
this.updateError(error);
}
}
static wrap(fn, value, options) {
return callable_default(new Computed(fn, value, options).observable);
}
};
var computed_default = Computed;
// src/disposed.ts
var disposed = () => {
const value = src_default(false);
context_default.registerCleanup(() => {
value(true);
});
return value;
};
var disposed_default = disposed;
// src/effect.ts
var Effect = class extends observer_default {
constructor(fn) {
super();
this.fn = fn;
this.update();
}
isDisposable() {
const { observers, observables, cleanups } = this;
if (observers) {
if (isArray(observers)) {
if (observers.length) {
return false;
}
} else {
return false;
}
}
if (observables) {
if (isArray(observables)) {
if (observables.length) {
return false;
}
} else {
return false;
}
}
if (cleanups) {
if (isArray(cleanups)) {
if (cleanups.length) {
return false;
}
} else {
return false;
}
}
return true;
}
update() {
context_default.registerObserver(this);
observer_default.unsubscribe(this);
delete this.dirty;
try {
const cleanup = context_default.wrapWith(() => this.fn(), this, false);
if (cleanup) {
this.registerCleanup(cleanup);
} else {
if (this.isDisposable()) {
context_default.unregisterObserver(this);
observer_default.unsubscribe(this);
}
}
} catch (error) {
this.updateError(error);
}
}
static wrap(fn) {
new Effect(fn);
}
};
var effect_default = Effect;
// src/from.ts
var from = (fn, options) => {
const value = src_default(void 0, options);
effect_default.wrap(() => fn(value));
return value;
};
var from_default = from;
// src/is.ts
var is = (value) => {
return isFunction(value) && !!value[SYMBOL];
};
var is_default = is;
// src/get.ts
var get = (value) => {
if (is_default(value))
return get(value());
return value;
};
var get_default = get;
// src/index.ts
function observable(value, options) {
return callable_default(new observable_default(value, options));
}
observable.batch = batch_default.wrap;
observable.cleanup = context_default.registerCleanup;
observable.computed = computed_default.wrap;
observable.disposed = disposed_default;
observable.effect = effect_default.wrap;
observable.error = context_default.registerError;
observable.from = from_default;
observable.get = get_default;
observable.is = is_default;
observable.root = context_default.wrapVoid;
observable.sample = context_default.wrapWithSampling;
var src_default = observable;
export {
src_default as default
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment