From ac769e656b122ff569c3f1534701b71e00fed586 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- FrontEnd/src/app/services/DataHub.ts | 225 +++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 FrontEnd/src/app/services/DataHub.ts (limited to 'FrontEnd/src/app/services/DataHub.ts') diff --git a/FrontEnd/src/app/services/DataHub.ts b/FrontEnd/src/app/services/DataHub.ts new file mode 100644 index 00000000..93a9b41f --- /dev/null +++ b/FrontEnd/src/app/services/DataHub.ts @@ -0,0 +1,225 @@ +import { pull } from "lodash"; +import { Observable, BehaviorSubject, combineLatest } from "rxjs"; +import { map } from "rxjs/operators"; + +export type Subscriber = (data: TData) => void; + +export type WithSyncStatus = T & { syncing: boolean }; + +export class DataLine { + private _current: TData | undefined = undefined; + + private _syncPromise: Promise | null = null; + private _syncingSubject = new BehaviorSubject(false); + + private _observers: Subscriber[] = []; + + constructor( + private config: { + sync: () => Promise; + destroyable?: (value: TData | undefined) => boolean; + disableInitSync?: boolean; + } + ) { + if (config.disableInitSync !== true) { + setImmediate(() => void this.sync()); + } + } + + private subscribe(subscriber: Subscriber): void { + this._observers.push(subscriber); + if (this._current !== undefined) { + subscriber(this._current); + } + } + + private unsubscribe(subscriber: Subscriber): void { + if (!this._observers.includes(subscriber)) return; + pull(this._observers, subscriber); + } + + getObservable(): Observable { + return new Observable((observer) => { + const f = (data: TData): void => { + observer.next(data); + }; + this.subscribe(f); + + return () => { + this.unsubscribe(f); + }; + }); + } + + getSyncStatusObservable(): Observable { + return this._syncingSubject.asObservable(); + } + + getDataWithSyncStatusObservable(): Observable> { + return combineLatest([ + this.getObservable(), + this.getSyncStatusObservable(), + ]).pipe( + map(([data, syncing]) => ({ + ...data, + syncing, + })) + ); + } + + get value(): TData | undefined { + return this._current; + } + + next(value: TData): void { + this._current = value; + this._observers.forEach((observer) => observer(value)); + } + + get isSyncing(): boolean { + return this._syncPromise != null; + } + + sync(): Promise { + if (this._syncPromise == null) { + this._syncingSubject.next(true); + this._syncPromise = this.config.sync().then(() => { + this._syncingSubject.next(false); + this._syncPromise = null; + }); + } + + return this._syncPromise; + } + + syncWithAction( + syncAction: (line: DataLine) => Promise + ): Promise { + if (this._syncPromise == null) { + this._syncingSubject.next(true); + this._syncPromise = syncAction(this).then(() => { + this._syncingSubject.next(false); + this._syncPromise = null; + }); + } + + return this._syncPromise; + } + + get destroyable(): boolean { + const customDestroyable = this.config?.destroyable; + + return ( + this._observers.length === 0 && + !this.isSyncing && + (customDestroyable != null ? customDestroyable(this._current) : true) + ); + } +} + +export class DataHub { + private sync: (key: TKey, line: DataLine) => Promise; + private keyToString: (key: TKey) => string; + private destroyable?: (key: TKey, value: TData | undefined) => boolean; + + private readonly subscriptionLineMap = new Map>(); + + 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(config: { + sync: (key: TKey, line: DataLine) => Promise; + keyToString?: (key: TKey) => string; + destroyable?: (key: TKey, value: TData | undefined) => boolean; + }) { + this.sync = config.sync; + this.keyToString = + config.keyToString ?? + ((value): string => { + if (typeof value === "string") return value; + else + throw new Error( + "Default keyToString function only pass string value." + ); + }); + + this.destroyable = config.destroyable; + } + + private cleanLines(): void { + const toDelete: string[] = []; + for (const [key, line] of this.subscriptionLineMap.entries()) { + if (line.destroyable) { + 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, disableInitSync = false): DataLine { + const keyString = this.keyToString(key); + const { destroyable } = this; + const newLine: DataLine = new DataLine({ + sync: () => this.sync(key, newLine), + destroyable: + destroyable != null ? (value) => destroyable(key, value) : undefined, + disableInitSync: disableInitSync, + }); + this.subscriptionLineMap.set(keyString, newLine); + if (this.subscriptionLineMap.size === 1) { + this.cleanTimerId = window.setInterval(this.cleanLines.bind(this), 20000); + } + return newLine; + } + + getObservable(key: TKey): Observable { + return this.getLineOrCreate(key).getObservable(); + } + + getSyncStatusObservable(key: TKey): Observable { + return this.getLineOrCreate(key).getSyncStatusObservable(); + } + + getDataWithSyncStatusObservable( + key: TKey + ): Observable> { + return this.getLineOrCreate(key).getDataWithSyncStatusObservable(); + } + + getLine(key: TKey): DataLine | null { + const keyString = this.keyToString(key); + return this.subscriptionLineMap.get(keyString) ?? null; + } + + getLineOrCreate(key: TKey): DataLine { + const keyString = this.keyToString(key); + return this.subscriptionLineMap.get(keyString) ?? this.createLine(key); + } + + getLineOrCreateWithoutInitSync(key: TKey): DataLine { + const keyString = this.keyToString(key); + return ( + this.subscriptionLineMap.get(keyString) ?? this.createLine(key, true) + ); + } + + optionalInitLineWithSyncAction( + key: TKey, + syncAction: (line: DataLine) => Promise + ): Promise { + const optionalLine = this.getLine(key); + if (optionalLine != null) return Promise.resolve(); + const line = this.createLine(key, true); + return line.syncWithAction(syncAction); + } +} -- cgit v1.2.3 From 5875e7a19ff8eb244e2849647ba35aa898de6b52 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 31 Oct 2020 00:37:10 +0800 Subject: Update packages. --- FrontEnd/package.json | 24 ++++++++++++------------ FrontEnd/src/app/services/DataHub.ts | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) (limited to 'FrontEnd/src/app/services/DataHub.ts') diff --git a/FrontEnd/package.json b/FrontEnd/package.json index 12f3fbea..d9136b6a 100644 --- a/FrontEnd/package.json +++ b/FrontEnd/package.json @@ -8,7 +8,7 @@ "dependencies": { "axios": "^0.21.0", "bootstrap": "^4.5.3", - "bootstrap-icons": "^1.0.0", + "bootstrap-icons": "^1.1.0", "classnames": "^2.2.6", "clsx": "^1.1.1", "core-js": "^3.6.5", @@ -32,7 +32,7 @@ "workbox-routing": "^5.1.4", "workbox-strategies": "^5.1.4", "workbox-window": "^5.1.4", - "xregexp": "^4.3.0" + "xregexp": "^4.4.0" }, "scripts": { "start": "webpack serve --config ./webpack.config.dev.js", @@ -62,11 +62,11 @@ "@babel/preset-react": "^7.12.1", "@babel/preset-typescript": "^7.12.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.4.2", - "@types/classnames": "^2.2.10", - "@types/lodash": "^4.14.162", - "@types/node": "^14.14.5", - "@types/react": "^16.9.53", - "@types/react-dom": "^16.9.8", + "@types/classnames": "^2.2.11", + "@types/lodash": "^4.14.164", + "@types/node": "^14.14.6", + "@types/react": "^16.9.55", + "@types/react-dom": "^16.9.9", "@types/react-responsive": "^8.0.2", "@types/react-router": "^5.1.8", "@types/react-router-bootstrap": "^0.24.5", @@ -82,29 +82,29 @@ "copy-webpack-plugin": "^6.2.1", "css-loader": "^5.0.0", "eslint": "^7.12.1", - "eslint-config-prettier": "^6.14.0", + "eslint-config-prettier": "^6.15.0", "eslint-plugin-import": "^2.22.1", "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-react": "^7.21.5", "eslint-plugin-react-hooks": "^4.2.0", - "file-loader": "^6.1.1", + "file-loader": "^6.2.0", "html-webpack-plugin": "^4.5.0", "http-server": "^0.12.3", - "mini-css-extract-plugin": "^1.2.0", + "mini-css-extract-plugin": "^1.2.1", "postcss": "^8.1.4", "postcss-loader": "^4.0.4", "postcss-preset-env": "^6.7.0", "prettier": "^2.1.2", "querystring-es3": "^0.2.1", "react-refresh": "^0.9.0", - "sass": "^1.27.0", + "sass": "^1.28.0", "sass-loader": "^10.0.4", "style-loader": "^2.0.0", "ts-loader": "^8.0.7", "type-fest": "^0.18.0", "typescript": "^4.0.5", "url-loader": "^4.1.1", - "webpack": "^5.2.1", + "webpack": "^5.3.2", "webpack-chain": "^6.5.1", "webpack-cli": "^4.1.0", "webpack-dev-server": "^3.11.0", diff --git a/FrontEnd/src/app/services/DataHub.ts b/FrontEnd/src/app/services/DataHub.ts index 93a9b41f..4d618db6 100644 --- a/FrontEnd/src/app/services/DataHub.ts +++ b/FrontEnd/src/app/services/DataHub.ts @@ -22,7 +22,7 @@ export class DataLine { } ) { if (config.disableInitSync !== true) { - setImmediate(() => void this.sync()); + setTimeout(() => void this.sync()); } } -- cgit v1.2.3