move observable-api to libs

This commit is contained in:
Jacob Overgaard
2023-01-23 14:28:44 +01:00
parent ac9a406eb9
commit 01d23848b9
10 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
import { distinctUntilChanged, map, Observable, shareReplay } from "rxjs";
import { MappingFunction, MemoizationFunction, defaultMemoization } from "./unique-behavior-subject";
/**
* @export
* @method createObservablePart
* @param {Observable<T>} source - RxJS Subject to use for this Observable.
* @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return.
* @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different.
* @description - Creates a RxJS Observable from RxJS Subject.
* @example <caption>Example create a Observable for part of the data Subject.</caption>
* public readonly myPart = CreateObservablePart(this._data, (data) => data.myPart);
*/
export function createObservablePart<T, R>(
source$: Observable<T>,
mappingFunction: MappingFunction<T, R>,
memoizationFunction?: MemoizationFunction<R>
): Observable<R> {
return source$.pipe(
map(mappingFunction),
distinctUntilChanged(memoizationFunction || defaultMemoization),
shareReplay(1)
);
}

View File

@@ -0,0 +1,6 @@
export * from './observer.controller';
export * from './observer';
export * from './unique-behavior-subject';
export * from './unique-array-behavior-subject';
export * from './unique-object-behavior-subject';
export * from './create-observable-part.method'

View File

@@ -0,0 +1,29 @@
import { Observable } from 'rxjs';
import { UmbObserver } from './observer';
import { UmbControllerInterface, UmbControllerHostInterface } from '@umbraco-cms/controller';
export class UmbObserverController<T> extends UmbObserver<T> implements UmbControllerInterface {
_alias?: string;
public get unique() {
return this._alias;
}
constructor(host: UmbControllerHostInterface, source: Observable<T>, callback: (_value: T) => void, alias?: string) {
super(source, callback);
this._alias = alias;
// Lets check if controller is already here:
/*
if (this._subscriptions.has(source)) {
const subscription = this._subscriptions.get(source);
subscription?.unsubscribe();
}
*/
host.addController(this);
}
hostConnected() {
return;
}
}

View File

@@ -0,0 +1,22 @@
import { Observable, Subscription } from 'rxjs';
export class UmbObserver<T> {
#subscription!: Subscription;
constructor(source: Observable<T>, callback: (_value: T) => void) {
this.#subscription = source.subscribe((value) => callback(value));
}
// Notice controller class implements empty hostConnected().
hostDisconnected() {
this.#subscription.unsubscribe();
}
destroy(): void {
this.#subscription.unsubscribe();
}
};

View File

@@ -0,0 +1,124 @@
import { expect } from '@open-wc/testing';
import { createObservablePart } from '@umbraco-cms/observable-api';
import { UniqueArrayBehaviorSubject } from './unique-array-behavior-subject';
describe('UniqueArrayBehaviorSubject', () => {
type ObjectType = {key: string, another: string};
type ArrayType = ObjectType[];
let subject: UniqueArrayBehaviorSubject<ObjectType>;
let initialData: ArrayType;
beforeEach(() => {
initialData = [
{key: '1', another: 'myValue1'},
{key: '2', another: 'myValue2'},
{key: '3', another: 'myValue3'}
];
subject = new UniqueArrayBehaviorSubject(initialData, (a, b) => a.key === b.key);
});
it('replays latests, no matter the amount of subscriptions.', (done) => {
const observer = subject.asObservable();
observer.subscribe((value) => {
expect(value).to.be.equal(initialData);
});
observer.subscribe((value) => {
expect(value).to.be.equal(initialData);
done();
});
});
it('add new item via appendOne method.', (done) => {
const newItem = {key: '4', another: 'myValue4'};
subject.appendOne(newItem);
const expectedData = [...initialData, newItem]
const observer = subject.asObservable();
observer.subscribe((value) => {
expect(value.length).to.be.equal(expectedData.length);
expect(value[3].another).to.be.equal(expectedData[3].another);
done();
});
});
it('createObservablePart for a specific entry of array', (done) => {
const subObserver = createObservablePart(subject, data => data.find(x => x.key === '2'));
subObserver.subscribe((entry) => {
if(entry) {
expect(entry.another).to.be.equal(initialData[1].another);
done();
}
});
});
it('createObservablePart returns undefined if item does not exist', (done) => {
let amountOfCallbacks = 0;
const newItem = {key: '4', another: 'myValue4'};
const subObserver = createObservablePart(subject, data => data.find(x => x.key === newItem.key));
subObserver.subscribe((entry) => {
amountOfCallbacks++;
if(amountOfCallbacks === 1) {
expect(entry).to.be.equal(undefined);// First callback should give null, cause we didn't have this entry when the subscription was made.
}
if(amountOfCallbacks === 2) {
expect(entry).to.be.equal(newItem);// Second callback should give us the right data:
if(entry) {
expect(entry.another).to.be.equal(newItem.another);
done();
}
}
});
subject.appendOne(newItem);
});
it('asObservable returns the replaced item', (done) => {
const newItem = {key: '2', another: 'myValue4'};
subject.appendOne(newItem);
const expectedData = [initialData[0], newItem, initialData[2]];
const observer = subject.asObservable();
observer.subscribe((value) => {
expect(value.length).to.be.equal(expectedData.length);
expect(value[1].another).to.be.equal(newItem.another);
done();
});
});
it('createObservablePart returns the replaced item', (done) => {
const newItem = {key: '2', another: 'myValue4'};
subject.appendOne(newItem);
const subObserver = createObservablePart(subject, data => data.find(x => x.key === newItem.key));
subObserver.subscribe((entry) => {
expect(entry).to.be.equal(newItem);// Second callback should give us the right data:
if(entry) {
expect(entry.another).to.be.equal(newItem.another);
done();
}
});
});
});

View File

@@ -0,0 +1,54 @@
import { appendToFrozenArray, UniqueBehaviorSubject } from "./unique-behavior-subject";
/**
* @export
* @class UniqueObjectBehaviorSubject
* @extends {UniqueBehaviorSubject<T>}
* @description - A RxJS UniqueObjectBehaviorSubject which deepFreezes the object-data to ensure its not manipulated from any implementations.
* Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content.
*
* The UniqueObjectBehaviorSubject provides methods to append data when the data is an Object.
*/
export class UniqueArrayBehaviorSubject<T> extends UniqueBehaviorSubject<T[]> {
constructor(initialData: T[], private _uniqueCompare?: (existingEntry: T, newEntry: T) => boolean) {
super(initialData);
}
/**
* @method append
* @param {Partial<T>} partialData - A object containing some of the data for this Subject.
* @description - Append some new data to this Object.
* @example <caption>Example append some data.</caption>
* const data = [
* { key: 1, value: 'foo'},
* { key: 2, value: 'bar'}
* ];
* const mySubject = new UniqueArrayBehaviorSubject(data);
* mySubject.append({ key: 1, value: 'replaced-foo'});
*/
appendOne(entry: T) {
this.next(appendToFrozenArray(this.getValue(), entry, this._uniqueCompare))
}
/**
* @method append
* @param {T[]} entries - A array of new data to be added in this Subject.
* @description - Append some new data to this Object, if it compares to existing data it will replace it.
* @example <caption>Example append some data.</caption>
* const data = [
* { key: 1, value: 'foo'},
* { key: 2, value: 'bar'}
* ];
* const mySubject = new UniqueArrayBehaviorSubject(data);
* mySubject.append([
* { key: 1, value: 'replaced-foo'},
* { key: 3, value: 'another-bla'}
* ]);
*/
append(entries: T[]) {
entries.forEach(x => this.appendOne(x))
}
}

View File

@@ -0,0 +1,61 @@
import { expect } from '@open-wc/testing';
import { UniqueBehaviorSubject } from './unique-behavior-subject';
import { createObservablePart } from '@umbraco-cms/observable-api';
describe('UniqueBehaviorSubject', () => {
type ObjectType = {key: string, another: string};
let subject: UniqueBehaviorSubject<ObjectType>;
let initialData: ObjectType;
beforeEach(() => {
initialData = {key: 'some', another: 'myValue'};
subject = new UniqueBehaviorSubject(initialData);
});
it('getValue gives the initial data', () => {
expect(subject.value.another).to.be.equal(initialData.another);
});
it('update via next', () => {
subject.next({key: 'some', another: 'myNewValue'});
expect(subject.value.another).to.be.equal('myNewValue');
});
it('replays latests, no matter the amount of subscriptions.', (done) => {
const observer = subject.asObservable();
observer.subscribe((value) => {
expect(value).to.be.equal(initialData);
});
observer.subscribe((value) => {
expect(value).to.be.equal(initialData);
done();
});
});
it('use createObservablePart, updates on its specific change.', (done) => {
let amountOfCallbacks = 0;
const subObserver = createObservablePart(subject, data => data.another);
subObserver.subscribe((value) => {
amountOfCallbacks++;
if(amountOfCallbacks === 1) {
expect(value).to.be.equal('myValue');
}
if(amountOfCallbacks === 2) {
expect(value).to.be.equal('myNewValue');
done();
}
});
subject.next({key: 'change_this_first_should_not_trigger_update', another: 'myValue'});
subject.next({key: 'some', another: 'myNewValue'});
});
});

View File

@@ -0,0 +1,90 @@
import { BehaviorSubject } from "rxjs";
// TODO: Should this handle array as well?
function deepFreeze<T>(inObj: T): T {
if(typeof inObj === 'object') {
Object.freeze(inObj);
Object.getOwnPropertyNames(inObj)?.forEach(function (prop) {
// eslint-disable-next-line no-prototype-builtins
if ((inObj as any).hasOwnProperty(prop)
&& (inObj as any)[prop] != null
&& typeof (inObj as any)[prop] === 'object'
&& !Object.isFrozen((inObj as any)[prop])) {
deepFreeze((inObj as any)[prop]);
}
});
}
return inObj;
}
export function naiveObjectComparison(objOne: any, objTwo: any): boolean {
return JSON.stringify(objOne) === JSON.stringify(objTwo);
}
/**
* @export
* @method appendToFrozenArray
* @param {Observable<T>} source - RxJS Subject to use for this Observable.
* @param {(mappable: T) => R} mappingFunction - Method to return the part for this Observable to return.
* @param {(previousResult: R, currentResult: R) => boolean} [memoizationFunction] - Method to Compare if the data has changed. Should return true when data is different.
* @description - Creates a RxJS Observable from RxJS Subject.
* @example <caption>Example append new entry for a UniqueBehaviorSubject which is an array. Where the key is unique and the item will be updated if matched with existing.</caption>
* const entry = {key: 'myKey', value: 'myValue'};
* const newDataSet = appendToFrozenArray(mySubject.getValue(), entry, x => x.key === key);
* mySubject.next(newDataSet);
*/
export function appendToFrozenArray<T>(data: T[], entry: T, uniqueMethod?: (existingEntry: T, newEntry: T) => boolean): T[] {
const unFrozenDataSet = [...data];
if(uniqueMethod) {
const indexToReplace = unFrozenDataSet.findIndex((x) => uniqueMethod(x, entry));
if(indexToReplace !== -1) {
unFrozenDataSet[indexToReplace] = entry;
} else {
unFrozenDataSet.push(entry);
}
} else {
unFrozenDataSet.push(entry);
}
return unFrozenDataSet;
}
export type MappingFunction<T, R> = (mappable: T) => R;
export type MemoizationFunction<R> = (previousResult: R, currentResult: R) => boolean;
export function defaultMemoization(previousValue: any, currentValue: any): boolean {
if (typeof previousValue === 'object' && typeof currentValue === 'object') {
return naiveObjectComparison(previousValue, currentValue);
}
return previousValue === currentValue;
}
/**
* @export
* @class UniqueBehaviorSubject
* @extends {BehaviorSubject<T>}
* @description - A RxJS BehaviorSubject which deepFreezes the data to ensure its not manipulated from any implementations.
* Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content.
*/
export class UniqueBehaviorSubject<T> extends BehaviorSubject<T> {
constructor(initialData: T) {
super(deepFreeze(initialData));
}
next(newData: T): void {
const frozenData = deepFreeze(newData);
// Only update data if its different than current data.
if (!naiveObjectComparison(frozenData, this.getValue())) {
super.next(frozenData);
}
}
}

View File

@@ -0,0 +1,52 @@
import { expect } from '@open-wc/testing';
import { UniqueObjectBehaviorSubject } from './unique-object-behavior-subject';
import { createObservablePart } from '@umbraco-cms/observable-api';
describe('UniqueObjectBehaviorSubject', () => {
type ObjectType = {key: string, another: string};
let subject: UniqueObjectBehaviorSubject<ObjectType>;
let initialData: ObjectType;
beforeEach(() => {
initialData = {key: 'some', another: 'myValue'};
subject = new UniqueObjectBehaviorSubject(initialData);
});
it('replays latests, no matter the amount of subscriptions.', (done) => {
const observer = subject.asObservable();
observer.subscribe((value) => {
expect(value).to.be.equal(initialData);
});
observer.subscribe((value) => {
expect(value).to.be.equal(initialData);
done();
});
});
it('use createObservablePart, updates on its specific change.', (done) => {
let amountOfCallbacks = 0;
const subObserver = createObservablePart(subject, data => data.another);
subObserver.subscribe((value) => {
amountOfCallbacks++;
if(amountOfCallbacks === 1) {
expect(value).to.be.equal('myValue');
}
if(amountOfCallbacks === 2) {
expect(value).to.be.equal('myNewValue');
done();
}
});
subject.update({key: 'change_this_first_should_not_trigger_update'});
subject.update({another: 'myNewValue'});
});
});

View File

@@ -0,0 +1,26 @@
import { UniqueBehaviorSubject } from "./unique-behavior-subject";
/**
* @export
* @class UniqueObjectBehaviorSubject
* @extends {UniqueBehaviorSubject<T>}
* @description - A RxJS UniqueObjectBehaviorSubject which deepFreezes the object-data to ensure its not manipulated from any implementations.
* Additionally the Subject ensures the data is unique, not updating any Observes unless there is an actual change of the content.
*
* The UniqueObjectBehaviorSubject provides methods to append data when the data is an Object.
*/
export class UniqueObjectBehaviorSubject<T> extends UniqueBehaviorSubject<T> {
/**
* @method append
* @param {Partial<T>} partialData - A object containing some of the data for this Subject.
* @description - Append some new data to this Object.
* @example <caption>Example append some data.</caption>
* const data = {key: 'myKey', value: 'myInitialValue'};
* const mySubject = new UniqueObjectBehaviorSubject(data)
* mySubject.append({value: 'myNewValue'})
*/
update(partialData: Partial<T>) {
this.next({ ...this.getValue(), ...partialData });
}
}