aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src/utils
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-13 02:07:04 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2021-12-14 02:14:23 +0100
commita96c52b21e7e590bbdd70b80896780a446fa2e8b (patch)
tree663619baa254577bb2f5342192e80bca692ad91d /subprojects/frontend/src/utils
parentbuild: move modules into subproject directory (diff)
downloadrefinery-a96c52b21e7e590bbdd70b80896780a446fa2e8b.tar.gz
refinery-a96c52b21e7e590bbdd70b80896780a446fa2e8b.tar.zst
refinery-a96c52b21e7e590bbdd70b80896780a446fa2e8b.zip
build: separate module for frontend
This allows us to simplify the webpack configuration and the gradle build scripts.
Diffstat (limited to 'subprojects/frontend/src/utils')
-rw-r--r--subprojects/frontend/src/utils/ConditionVariable.ts64
-rw-r--r--subprojects/frontend/src/utils/PendingTask.ts60
-rw-r--r--subprojects/frontend/src/utils/Timer.ts33
-rw-r--r--subprojects/frontend/src/utils/logger.ts49
4 files changed, 206 insertions, 0 deletions
diff --git a/subprojects/frontend/src/utils/ConditionVariable.ts b/subprojects/frontend/src/utils/ConditionVariable.ts
new file mode 100644
index 00000000..0910dfa6
--- /dev/null
+++ b/subprojects/frontend/src/utils/ConditionVariable.ts
@@ -0,0 +1,64 @@
1import { getLogger } from './logger';
2import { PendingTask } from './PendingTask';
3
4const log = getLogger('utils.ConditionVariable');
5
6export type Condition = () => boolean;
7
8export class ConditionVariable {
9 condition: Condition;
10
11 defaultTimeout: number;
12
13 listeners: PendingTask<void>[] = [];
14
15 constructor(condition: Condition, defaultTimeout = 0) {
16 this.condition = condition;
17 this.defaultTimeout = defaultTimeout;
18 }
19
20 async waitFor(timeoutMs: number | null = null): Promise<void> {
21 if (this.condition()) {
22 return;
23 }
24 const timeoutOrDefault = timeoutMs || this.defaultTimeout;
25 let nowMs = Date.now();
26 const endMs = nowMs + timeoutOrDefault;
27 while (!this.condition() && nowMs < endMs) {
28 const remainingMs = endMs - nowMs;
29 const promise = new Promise<void>((resolve, reject) => {
30 if (this.condition()) {
31 resolve();
32 return;
33 }
34 const task = new PendingTask(resolve, reject, remainingMs);
35 this.listeners.push(task);
36 });
37 // We must keep waiting until the update has completed,
38 // so the tasks can't be started in parallel.
39 // eslint-disable-next-line no-await-in-loop
40 await promise;
41 nowMs = Date.now();
42 }
43 if (!this.condition()) {
44 log.error('Condition still does not hold after', timeoutOrDefault, 'ms');
45 throw new Error('Failed to wait for condition');
46 }
47 }
48
49 notifyAll(): void {
50 this.clearListenersWith((listener) => listener.resolve());
51 }
52
53 rejectAll(error: unknown): void {
54 this.clearListenersWith((listener) => listener.reject(error));
55 }
56
57 private clearListenersWith(callback: (listener: PendingTask<void>) => void) {
58 // Copy `listeners` so that we don't get into a race condition
59 // if one of the listeners adds another listener.
60 const { listeners } = this;
61 this.listeners = [];
62 listeners.forEach(callback);
63 }
64}
diff --git a/subprojects/frontend/src/utils/PendingTask.ts b/subprojects/frontend/src/utils/PendingTask.ts
new file mode 100644
index 00000000..51b79fb0
--- /dev/null
+++ b/subprojects/frontend/src/utils/PendingTask.ts
@@ -0,0 +1,60 @@
1import { getLogger } from './logger';
2
3const log = getLogger('utils.PendingTask');
4
5export class PendingTask<T> {
6 private readonly resolveCallback: (value: T) => void;
7
8 private readonly rejectCallback: (reason?: unknown) => void;
9
10 private resolved = false;
11
12 private timeout: number | null;
13
14 constructor(
15 resolveCallback: (value: T) => void,
16 rejectCallback: (reason?: unknown) => void,
17 timeoutMs?: number,
18 timeoutCallback?: () => void,
19 ) {
20 this.resolveCallback = resolveCallback;
21 this.rejectCallback = rejectCallback;
22 if (timeoutMs) {
23 this.timeout = setTimeout(() => {
24 if (!this.resolved) {
25 this.reject(new Error('Request timed out'));
26 if (timeoutCallback) {
27 timeoutCallback();
28 }
29 }
30 }, timeoutMs);
31 } else {
32 this.timeout = null;
33 }
34 }
35
36 resolve(value: T): void {
37 if (this.resolved) {
38 log.warn('Trying to resolve already resolved promise');
39 return;
40 }
41 this.markResolved();
42 this.resolveCallback(value);
43 }
44
45 reject(reason?: unknown): void {
46 if (this.resolved) {
47 log.warn('Trying to reject already resolved promise');
48 return;
49 }
50 this.markResolved();
51 this.rejectCallback(reason);
52 }
53
54 private markResolved() {
55 this.resolved = true;
56 if (this.timeout !== null) {
57 clearTimeout(this.timeout);
58 }
59 }
60}
diff --git a/subprojects/frontend/src/utils/Timer.ts b/subprojects/frontend/src/utils/Timer.ts
new file mode 100644
index 00000000..8f653070
--- /dev/null
+++ b/subprojects/frontend/src/utils/Timer.ts
@@ -0,0 +1,33 @@
1export class Timer {
2 readonly callback: () => void;
3
4 readonly defaultTimeout: number;
5
6 timeout: number | null = null;
7
8 constructor(callback: () => void, defaultTimeout = 0) {
9 this.callback = () => {
10 this.timeout = null;
11 callback();
12 };
13 this.defaultTimeout = defaultTimeout;
14 }
15
16 schedule(timeout: number | null = null): void {
17 if (this.timeout === null) {
18 this.timeout = setTimeout(this.callback, timeout || this.defaultTimeout);
19 }
20 }
21
22 reschedule(timeout: number | null = null): void {
23 this.cancel();
24 this.schedule(timeout);
25 }
26
27 cancel(): void {
28 if (this.timeout !== null) {
29 clearTimeout(this.timeout);
30 this.timeout = null;
31 }
32 }
33}
diff --git a/subprojects/frontend/src/utils/logger.ts b/subprojects/frontend/src/utils/logger.ts
new file mode 100644
index 00000000..306d122c
--- /dev/null
+++ b/subprojects/frontend/src/utils/logger.ts
@@ -0,0 +1,49 @@
1import styles, { CSPair } from 'ansi-styles';
2import log from 'loglevel';
3import * as prefix from 'loglevel-plugin-prefix';
4
5const colors: Partial<Record<string, CSPair>> = {
6 TRACE: styles.magenta,
7 DEBUG: styles.cyan,
8 INFO: styles.blue,
9 WARN: styles.yellow,
10 ERROR: styles.red,
11};
12
13prefix.reg(log);
14
15if (DEBUG) {
16 log.setLevel(log.levels.DEBUG);
17} else {
18 log.setLevel(log.levels.WARN);
19}
20
21if ('chrome' in window) {
22 // Only Chromium supports console ANSI escape sequences.
23 prefix.apply(log, {
24 format(level, name, timestamp) {
25 const formattedTimestamp = `${styles.gray.open}[${timestamp.toString()}]${styles.gray.close}`;
26 const levelColor = colors[level.toUpperCase()] || styles.red;
27 const formattedLevel = `${levelColor.open}${level}${levelColor.close}`;
28 const formattedName = `${styles.green.open}(${name || 'root'})${styles.green.close}`;
29 return `${formattedTimestamp} ${formattedLevel} ${formattedName}`;
30 },
31 });
32} else {
33 prefix.apply(log, {
34 template: '[%t] %l (%n)',
35 });
36}
37
38const appLogger = log.getLogger(PACKAGE_NAME);
39
40appLogger.info('Version:', PACKAGE_NAME, PACKAGE_VERSION);
41appLogger.info('Debug mode:', DEBUG);
42
43export function getLoggerFromRoot(name: string | symbol): log.Logger {
44 return log.getLogger(name);
45}
46
47export function getLogger(name: string | symbol): log.Logger {
48 return getLoggerFromRoot(`${PACKAGE_NAME}.${name.toString()}`);
49}