aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/app/services/DataHub2.ts
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/app/services/DataHub2.ts')
-rw-r--r--FrontEnd/src/app/services/DataHub2.ts181
1 files changed, 181 insertions, 0 deletions
diff --git a/FrontEnd/src/app/services/DataHub2.ts b/FrontEnd/src/app/services/DataHub2.ts
new file mode 100644
index 00000000..50ae919b
--- /dev/null
+++ b/FrontEnd/src/app/services/DataHub2.ts
@@ -0,0 +1,181 @@
+import { Observable } from "rxjs";
+
+export type DataStatus = "syncing" | "synced" | "offline";
+
+export function mergeDataStatus(statusList: DataStatus[]): DataStatus {
+ if (statusList.includes("offline")) {
+ return "offline";
+ } else if (statusList.includes("syncing")) {
+ return "syncing";
+ } else {
+ return "synced";
+ }
+}
+
+export type Subscriber<TData> = (data: TData) => void;
+
+export interface DataAndStatus<TData> {
+ data: TData | null;
+ status: DataStatus;
+}
+
+export class DataLine2<TData> {
+ constructor(
+ private config: {
+ saveData: (data: TData) => Promise<void>;
+ getSavedData: () => Promise<TData | null>;
+ // return null for offline
+ fetchData: (savedData: TData | null) => Promise<TData | null>;
+ }
+ ) {}
+
+ private _current: DataAndStatus<TData> | null = null;
+ private _observers: Subscriber<DataAndStatus<TData>>[] = [];
+
+ get currentData(): DataAndStatus<TData> | null {
+ return this._current;
+ }
+
+ get isDestroyable(): boolean {
+ const { _observers, currentData } = this;
+ return (
+ _observers.length === 0 &&
+ (currentData == null || currentData.status !== "syncing")
+ );
+ }
+
+ private next(data: DataAndStatus<TData>): void {
+ this._current = data;
+ this._observers.forEach((o) => o(data));
+ }
+
+ subscribe(subsriber: Subscriber<DataAndStatus<TData>>): void {
+ this.sync(); // TODO: Should I sync at this point or let the user sync explicitly.
+ this._observers.push(subsriber);
+ const { currentData } = this;
+ if (currentData != null) {
+ subsriber(currentData);
+ }
+ }
+
+ unsubscribe(subsriber: Subscriber<DataAndStatus<TData>>): void {
+ const index = this._observers.indexOf(subsriber);
+ if (index > -1) this._observers.splice(index, 1);
+ }
+
+ getObservalble(): Observable<DataAndStatus<TData>> {
+ return new Observable<DataAndStatus<TData>>((observer) => {
+ const f = (data: DataAndStatus<TData>): void => {
+ observer.next(data);
+ };
+ this.subscribe(f);
+
+ return () => {
+ this.unsubscribe(f);
+ };
+ });
+ }
+
+ sync(): void {
+ const { currentData } = this;
+ if (currentData != null && currentData.status === "syncing") return;
+ this.next({ data: currentData?.data ?? null, status: "syncing" });
+ void this.config.getSavedData().then((savedData) => {
+ if (currentData == null && savedData != null) {
+ this.next({ data: savedData, status: "syncing" });
+ }
+ return this.config.fetchData(savedData).then((data) => {
+ if (data == null) {
+ this.next({
+ data: savedData,
+ status: "offline",
+ });
+ } else {
+ return this.config.saveData(data).then(() => {
+ this.next({ data: data, status: "synced" });
+ });
+ }
+ });
+ });
+ }
+
+ save(data: TData): void {
+ const { currentData } = this;
+ if (currentData != null && currentData.status === "syncing") return;
+ this.next({ data: currentData?.data ?? null, status: "syncing" });
+ void this.config.saveData(data).then(() => {
+ this.next({ data: data, status: "synced" });
+ });
+ }
+
+ getSavedData(): Promise<TData | null> {
+ return this.config.getSavedData();
+ }
+}
+
+export class DataHub2<TKey, TData> {
+ private readonly subscriptionLineMap = new Map<string, DataLine2<TData>>();
+
+ private keyToString: (key: TKey) => string;
+
+ private cleanTimerId = 0;
+
+ // setup is called after creating line and if it returns a function as destroyer, then when the line is destroyed the destroyer will be called.
+ constructor(
+ private config: {
+ saveData: (key: TKey, data: TData) => Promise<void>;
+ getSavedData: (key: TKey) => Promise<TData | null>;
+ fetchData: (key: TKey, savedData: TData | null) => Promise<TData | null>;
+ keyToString?: (key: TKey) => string;
+ }
+ ) {
+ this.keyToString =
+ config.keyToString ??
+ ((value): string => {
+ if (typeof value === "string") return value;
+ else
+ throw new Error(
+ "Default keyToString function only pass string value."
+ );
+ });
+ }
+
+ private cleanLines(): void {
+ const toDelete: string[] = [];
+ for (const [key, line] of this.subscriptionLineMap.entries()) {
+ if (line.isDestroyable) {
+ toDelete.push(key);
+ }
+ }
+
+ if (toDelete.length === 0) return;
+
+ for (const key of toDelete) {
+ this.subscriptionLineMap.delete(key);
+ }
+
+ if (this.subscriptionLineMap.size === 0) {
+ window.clearInterval(this.cleanTimerId);
+ this.cleanTimerId = 0;
+ }
+ }
+
+ private createLine(key: TKey): DataLine2<TData> {
+ const keyString = this.keyToString(key);
+ const newLine: DataLine2<TData> = new DataLine2<TData>({
+ saveData: (data) => this.config.saveData(key, data),
+ getSavedData: () => this.config.getSavedData(key),
+ fetchData: (savedData) => this.config.fetchData(key, savedData),
+ });
+ this.subscriptionLineMap.set(keyString, newLine);
+ if (this.subscriptionLineMap.size === 1) {
+ this.cleanTimerId = window.setInterval(this.cleanLines.bind(this), 20000);
+ }
+ return newLine;
+ }
+
+ getLine(key: TKey): DataLine2<TData> {
+ const keyString = this.keyToString(key);
+ return this.subscriptionLineMap.get(keyString) ?? this.createLine(key);
+ }
+}