mirror of
https://github.com/morten-olsen/morten-olsen.github.io.git
synced 2026-02-08 01:46:28 +01:00
init
This commit is contained in:
70
bin/observable/index.test.ts
Normal file
70
bin/observable/index.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Observable } from "./observable";
|
||||
import { getCollectionItems } from "./utils";
|
||||
|
||||
describe("observable", () => {
|
||||
it("should be able to create an observable", async () => {
|
||||
const observable = new Observable(() => Promise.resolve(1));
|
||||
expect(observable).toBeDefined();
|
||||
const data = await observable.data;
|
||||
expect(data).toBe(1);
|
||||
});
|
||||
|
||||
it("should be able to combine observables", async () => {
|
||||
const observable1 = new Observable(() => Promise.resolve(1));
|
||||
const observable2 = new Observable(() => Promise.resolve(2));
|
||||
const combined = Observable.combine({ observable1, observable2 });
|
||||
const data = await combined.data;
|
||||
expect(data.observable1).toBe(1);
|
||||
expect(data.observable2).toBe(2);
|
||||
});
|
||||
|
||||
it("should be able to update observable", async () => {
|
||||
const observable = new Observable(() => Promise.resolve(1));
|
||||
const data = await observable.data;
|
||||
expect(data).toBe(1);
|
||||
observable.set(() => Promise.resolve(2));
|
||||
const data2 = await observable.data;
|
||||
expect(data2).toBe(2);
|
||||
});
|
||||
|
||||
it("should be able to extract collection items", async () => {
|
||||
const observable = new Observable(() =>
|
||||
Promise.resolve([
|
||||
new Observable(() => Promise.resolve(1)),
|
||||
new Observable(() => Promise.resolve(2)),
|
||||
new Observable(() => Promise.resolve(3)),
|
||||
])
|
||||
);
|
||||
const flatten = observable.pipe(getCollectionItems);
|
||||
const data = await flatten.data;
|
||||
expect(data).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("should update observable when subscribed", async () => {
|
||||
const observable = new Observable(() => Promise.resolve(1));
|
||||
const spy = jest.fn();
|
||||
observable.subscribe(spy);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
observable.set(() => Promise.resolve(2));
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should update combined observable when subscribed", async () => {
|
||||
const observable1 = new Observable(() => Promise.resolve(1));
|
||||
const observable2 = new Observable(() => Promise.resolve(2));
|
||||
const combined = Observable.combine({ observable1, observable2 });
|
||||
const spy = jest.fn();
|
||||
const data1 = await combined.data;
|
||||
expect(data1.observable1).toBe(1);
|
||||
expect(data1.observable2).toBe(2);
|
||||
combined.subscribe(spy);
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
observable2.set(() => Promise.resolve(3));
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
const data2 = await combined.data;
|
||||
expect(data2.observable1).toBe(1);
|
||||
expect(data2.observable2).toBe(3);
|
||||
});
|
||||
});
|
||||
2
bin/observable/index.ts
Normal file
2
bin/observable/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Observable } from "./observable";
|
||||
export { getCollectionItems } from "./utils";
|
||||
81
bin/observable/observable.ts
Normal file
81
bin/observable/observable.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
type Observer = () => void;
|
||||
|
||||
type ObservableRecord<T extends Record<string, Observable<any>>> = {
|
||||
[K in keyof T]: T[K] extends Observable<infer U> ? U : never;
|
||||
};
|
||||
|
||||
class Observable<T> {
|
||||
#observers: Observer[] = [];
|
||||
#data?: Promise<T>;
|
||||
#loader: (current?: T) => Promise<T>;
|
||||
|
||||
constructor(loader: () => Promise<T>) {
|
||||
this.#loader = loader;
|
||||
}
|
||||
|
||||
public get ready() {
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
public get data() {
|
||||
if (!this.#data) {
|
||||
this.#data = this.#loader(this.#data);
|
||||
}
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
public recreate = () => {
|
||||
this.#data = undefined;
|
||||
this.notify();
|
||||
};
|
||||
|
||||
public set(loader: (current?: T) => Promise<T>) {
|
||||
this.#data = undefined;
|
||||
this.#loader = loader;
|
||||
this.notify();
|
||||
}
|
||||
|
||||
public notify = () => {
|
||||
this.#observers.forEach((observer) => observer());
|
||||
};
|
||||
|
||||
subscribe = (observer: Observer) => {
|
||||
this.#observers.push(observer);
|
||||
return () => this.unsubscribe(observer);
|
||||
};
|
||||
|
||||
unsubscribe = (observer: Observer) => {
|
||||
this.#observers = this.#observers.filter((o) => o !== observer);
|
||||
};
|
||||
|
||||
pipe = <U>(fn: (data: T) => Promise<U>) => {
|
||||
const loader = async () => fn(await this.data);
|
||||
const observable = new Observable<U>(loader);
|
||||
this.subscribe(() => {
|
||||
observable.set(loader);
|
||||
});
|
||||
return observable;
|
||||
};
|
||||
|
||||
static combine = <U extends Record<string, Observable<any>>>(
|
||||
record: U
|
||||
): Observable<ObservableRecord<U>> => {
|
||||
const loader = () =>
|
||||
Object.entries(record).reduce(
|
||||
async (accP, [key, value]) => ({
|
||||
...(await accP),
|
||||
[key]: await value.data,
|
||||
}),
|
||||
{} as any
|
||||
);
|
||||
const observable = new Observable<ObservableRecord<U>>(loader);
|
||||
Object.values(record).forEach((item) => {
|
||||
item.subscribe(async () => {
|
||||
observable.set(loader);
|
||||
});
|
||||
});
|
||||
return observable;
|
||||
};
|
||||
}
|
||||
|
||||
export { Observable };
|
||||
7
bin/observable/utils.ts
Normal file
7
bin/observable/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Observable } from "./observable";
|
||||
|
||||
const getCollectionItems = async <T>(items: Observable<T>[]) => {
|
||||
return Promise.all(items.map((item) => item.data));
|
||||
};
|
||||
|
||||
export { getCollectionItems };
|
||||
Reference in New Issue
Block a user