readiness-manager 1.1.3 → 1.2.0-rc.x6cd566eea
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -1
- package/README.md +14 -79
- package/dist/src/error.d.ts +16 -0
- package/dist/src/error.d.ts.map +1 -0
- package/dist/src/error.js +23 -0
- package/dist/src/error.js.map +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +126 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +3 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tests/mock.d.ts +8 -0
- package/dist/tests/mock.d.ts.map +1 -0
- package/dist/tests/mock.js +39 -0
- package/dist/tests/mock.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/jest.config.ts +8 -0
- package/package.json +16 -27
- package/project.json +16 -0
- package/src/error.ts +46 -0
- package/src/index.ts +230 -0
- package/src/spec.ts +250 -0
- package/src/types.ts +38 -0
- package/tests/mock.ts +57 -0
- package/tsconfig.json +10 -0
- package/LICENSE +0 -8
- package/index.js +0 -1
- package/src/constants.js +0 -14
- package/src/error.js +0 -42
- package/src/index.js +0 -208
- package/src/types.d.ts +0 -40
- package/tests/mock.d.ts +0 -9
- package/tests/mock.js +0 -36
package/jest.config.ts
ADDED
package/package.json
CHANGED
|
@@ -1,33 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "readiness-manager",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-rc.x6cd566eea",
|
|
4
4
|
"description": "👨💼 Define when your app is ready",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"handlers",
|
|
9
|
-
"process"
|
|
10
|
-
],
|
|
11
|
-
"author": "Fiverr Ants",
|
|
12
|
-
"license": "MIT",
|
|
13
|
-
"repository": {
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "git+https://github.com/fiverr/readiness-manager.git"
|
|
5
|
+
"main": "./dist/src/index.js",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@fiverr-private/obs": "^1.35.0"
|
|
16
8
|
},
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"test": "jest",
|
|
22
|
-
"lint": "eslint . --ext .js"
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/src/index.js",
|
|
11
|
+
"./tests/mock": "./dist/tests/mock.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
23
13
|
},
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"tag": "latest"
|
|
14
|
+
"types": "./dist/src/index.d.ts",
|
|
15
|
+
"nx": {
|
|
16
|
+
"tags": [
|
|
17
|
+
"scope:lib",
|
|
18
|
+
"package_core",
|
|
19
|
+
"package_core:lib"
|
|
20
|
+
]
|
|
32
21
|
}
|
|
33
|
-
}
|
|
22
|
+
}
|
package/project.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "readiness-manager",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"projectType": "library",
|
|
5
|
+
"sourceRoot": "packages/atoms/readiness-manager/src",
|
|
6
|
+
"targets": {
|
|
7
|
+
"build": {
|
|
8
|
+
"executor": "nx:run-commands",
|
|
9
|
+
"options": {
|
|
10
|
+
"commands": ["rm -rf ./dist", "tsc --build tsconfig.json"],
|
|
11
|
+
"parallel": false,
|
|
12
|
+
"cwd": "packages/atoms/readiness-manager"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
package/src/error.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { logger } from '@fiverr-private/obs';
|
|
2
|
+
/**
|
|
3
|
+
* The possible errors that could be thrown.
|
|
4
|
+
*/
|
|
5
|
+
export const ERRORS = {
|
|
6
|
+
BEACON_ALREADY_EXISTS: 'Beacon is already exists, please ensure to use a unique name per beacon',
|
|
7
|
+
BEACON_DOES_NOT_EXISTS:
|
|
8
|
+
'Beacon does not exists, make sure to call `register` before trying to set beacon ready hook.',
|
|
9
|
+
BEACON_EXECUTION_FAILED: 'Beacon execution failed',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The retry function to be triggered upon beacon errors.
|
|
14
|
+
* @returns {Promise<unknown>}
|
|
15
|
+
*/
|
|
16
|
+
export type ActionRetry = () => Promise<unknown>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The error handler to be triggered upon beacon errors.
|
|
20
|
+
*/
|
|
21
|
+
export type ActionErrorHandler = (error: ActionExecutionError, retry: ActionRetry) => void;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The custom error to be thrown when beacon execution fails.
|
|
25
|
+
*/
|
|
26
|
+
export class ActionExecutionError extends Error {
|
|
27
|
+
override name: string;
|
|
28
|
+
override stack: string;
|
|
29
|
+
attempt: number;
|
|
30
|
+
failReason: string;
|
|
31
|
+
|
|
32
|
+
constructor(name: string, attempt: number, error: Error) {
|
|
33
|
+
super();
|
|
34
|
+
|
|
35
|
+
this.message = ERRORS.BEACON_EXECUTION_FAILED;
|
|
36
|
+
this.name = name;
|
|
37
|
+
this.attempt = attempt;
|
|
38
|
+
this.stack = error.stack || '';
|
|
39
|
+
this.failReason = error.message;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Default error handler to be triggered upon beacon errors.
|
|
45
|
+
*/
|
|
46
|
+
export const defaultErrorHandler: ActionErrorHandler = (beaconError) => logger.error(beaconError);
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { logger } from '@fiverr-private/obs';
|
|
2
|
+
import { ERRORS, ActionExecutionError, defaultErrorHandler, ActionErrorHandler } from './error';
|
|
3
|
+
import type { ActionStatus, ReadyAction, ReadyCallback, ActionsStatus, BeaconsMap, Beacon } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The internal "ready" state to determine process readiness.
|
|
7
|
+
*/
|
|
8
|
+
let readyState = false;
|
|
9
|
+
|
|
10
|
+
export interface ReadinessManager {
|
|
11
|
+
register: (name: string, action: ReadyAction, debug?: boolean) => void;
|
|
12
|
+
run: () => ReadinessManager;
|
|
13
|
+
onReady: (callback: ReadyCallback) => ReadinessManager;
|
|
14
|
+
onActionReady: (name: string, callback: ReadyCallback) => ReadinessManager;
|
|
15
|
+
onError: (errorHandler: ActionErrorHandler) => ReadinessManager;
|
|
16
|
+
status: () => ActionsStatus;
|
|
17
|
+
ready: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The internal implementation of the ReadinessManager.
|
|
22
|
+
* @example
|
|
23
|
+
* const readinessManager = new ReadinessManager();
|
|
24
|
+
* readinessManager.register('database', () => connectToDB());
|
|
25
|
+
* readinessManager.register('redis', () => connectToRedis());
|
|
26
|
+
* readinessManager.run();
|
|
27
|
+
* readinessManager.onReady(() => console.log('App is ready! o/'));
|
|
28
|
+
* @returns {ReadinessManager}
|
|
29
|
+
*/
|
|
30
|
+
class ReadinessManagerImpl implements ReadinessManager {
|
|
31
|
+
#beacons: BeaconsMap;
|
|
32
|
+
#callbacks: ReadyCallback[];
|
|
33
|
+
#errorHandler: ActionErrorHandler;
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
this.#beacons = {};
|
|
37
|
+
this.#callbacks = [];
|
|
38
|
+
this.#errorHandler = defaultErrorHandler;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns `true` if all the registered actions has been ran without exceptions/rejections.
|
|
43
|
+
*/
|
|
44
|
+
get ready(): boolean {
|
|
45
|
+
return readyState;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns current registered actions status
|
|
50
|
+
*/
|
|
51
|
+
status(): ActionsStatus {
|
|
52
|
+
return Object.values(this.#beacons).reduce((acc, { name, status }) => {
|
|
53
|
+
const current = acc[status] || [];
|
|
54
|
+
return Object.assign(acc, { [status]: [...current, name] });
|
|
55
|
+
}, {} as ActionsStatus);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Registers a given name and action as beacon under manager.
|
|
60
|
+
* registered actions will be observed once ran in order to determine the process readiness state.
|
|
61
|
+
* @param name - The operation name to register with.
|
|
62
|
+
* @param action - The ready emitter to condition the process readiness with.
|
|
63
|
+
* @param debug - (Optional) flag to enable timing logs (default: `false`).
|
|
64
|
+
* @throws {Error} If the action name is already registered.
|
|
65
|
+
*/
|
|
66
|
+
register(name: string, action: ReadyAction, debug = false): void {
|
|
67
|
+
if (this.ready) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this.#beacons[name]) {
|
|
72
|
+
throw new Error(ERRORS.BEACON_ALREADY_EXISTS);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const beacon: Beacon = {
|
|
76
|
+
name,
|
|
77
|
+
action,
|
|
78
|
+
callbacks: [],
|
|
79
|
+
status: 'not_started',
|
|
80
|
+
debug,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
Object.assign(this.#beacons, { [name]: beacon });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
*
|
|
88
|
+
* Runs all registered actions. Once all actions are resolved successfully, your app state will be determined as `ready`.
|
|
89
|
+
* @returns {ReadinessManager}
|
|
90
|
+
*/
|
|
91
|
+
run(): ReadinessManager {
|
|
92
|
+
readyState = false;
|
|
93
|
+
|
|
94
|
+
Object.values(this.#beacons).forEach((beacon) => this.#execute(beacon));
|
|
95
|
+
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Registers a given callback on the global manager ready event.
|
|
101
|
+
* @param callback - The callback to run upon manager ready event.
|
|
102
|
+
* @returns {ReadinessManager}
|
|
103
|
+
*/
|
|
104
|
+
onReady(callback: ReadyCallback): ReadinessManager {
|
|
105
|
+
if (readyState) {
|
|
106
|
+
// Invokes immediately given callback if process is already ready.
|
|
107
|
+
callback();
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.#callbacks.push(callback);
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Registers a given callback on a specific action resolved.
|
|
117
|
+
* @param name - The name of the action the callback should be attached to.
|
|
118
|
+
* @param callback - The callback to run upon beacon resolved.
|
|
119
|
+
* @returns {ReadinessManager}
|
|
120
|
+
* @throws {Error} If the action name does not exists.
|
|
121
|
+
*/
|
|
122
|
+
onActionReady(name: string, callback: ReadyCallback): ReadinessManager {
|
|
123
|
+
const beacon = this.#beacons[name];
|
|
124
|
+
if (!beacon) {
|
|
125
|
+
throw new Error(ERRORS.BEACON_DOES_NOT_EXISTS);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (beacon.status === 'resolved') {
|
|
129
|
+
// Invokes immediately given callback if beacon status is already resolved.
|
|
130
|
+
callback();
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.#beacons[name].callbacks.push(callback);
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Registers a given error handler under manager errors.
|
|
140
|
+
* @param errorHandler - The handler to trigger upon action errors.
|
|
141
|
+
*/
|
|
142
|
+
onError(errorHandler: ActionErrorHandler): ReadinessManager {
|
|
143
|
+
this.#errorHandler = errorHandler;
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Tracks a given beacon action.
|
|
149
|
+
* @param beacon - The beacon to execute.
|
|
150
|
+
* @param attempt - The attempt number of current beacon execution.
|
|
151
|
+
* @throws {ActionExecutionError}
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
async #execute(beacon: Beacon, attempt = 1): Promise<void> {
|
|
155
|
+
const { name, action, debug } = beacon;
|
|
156
|
+
|
|
157
|
+
const update = (status: ActionStatus) => this.#updateBeacon(name, status);
|
|
158
|
+
|
|
159
|
+
update('pending');
|
|
160
|
+
|
|
161
|
+
const startTime = debug ? Date.now() : null;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// The actual beacon execution we want to keep track on.
|
|
165
|
+
const result = action();
|
|
166
|
+
|
|
167
|
+
if (result instanceof Promise) {
|
|
168
|
+
await result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (debug && startTime !== null) {
|
|
172
|
+
const duration = Date.now() - startTime;
|
|
173
|
+
logger.debug(`[ReadinessManager] Action "${name}" completed in ${duration}ms`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
update('resolved');
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (debug && startTime !== null) {
|
|
179
|
+
const duration = Date.now() - startTime;
|
|
180
|
+
logger.debug(`[ReadinessManager] Action "${name}" failed after ${duration}ms`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
update('rejected');
|
|
184
|
+
|
|
185
|
+
// Invokes consumer error hook with a retry method and the beacon error.
|
|
186
|
+
this.#errorHandler(new ActionExecutionError(name, attempt, error as Error), () =>
|
|
187
|
+
this.#execute(beacon, attempt + 1)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Updates a given beacon name with a given status.
|
|
194
|
+
* @param name - The beacon name to update.
|
|
195
|
+
* @param status - The beacon status to set.
|
|
196
|
+
* @private
|
|
197
|
+
*/
|
|
198
|
+
#updateBeacon(name: string, status: ActionStatus): void {
|
|
199
|
+
Object.assign(this.#beacons[name], { status });
|
|
200
|
+
|
|
201
|
+
// Resolves specific beacon ready listeners.
|
|
202
|
+
if (status === 'resolved') {
|
|
203
|
+
this.#beacons[name].callbacks.forEach((callback) => callback());
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.#trackBeaconUpdate();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Tracks a beacon update event, checks whether process is "ready".
|
|
211
|
+
* If so, will trigger registered `onReady` callbacks.
|
|
212
|
+
* @private
|
|
213
|
+
*/
|
|
214
|
+
#trackBeaconUpdate(): void {
|
|
215
|
+
if (readyState) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
readyState = Object.values(this.#beacons).every((beacon) => beacon.status === 'resolved');
|
|
220
|
+
|
|
221
|
+
if (readyState) {
|
|
222
|
+
this.#callbacks.forEach((callback) => callback());
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const manager: ReadinessManager = new ReadinessManagerImpl();
|
|
228
|
+
|
|
229
|
+
export default manager;
|
|
230
|
+
export { ActionExecutionError, ActionErrorHandler };
|
package/src/spec.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { ERRORS, ActionExecutionError } from './error';
|
|
2
|
+
import { ReadinessManager } from './index';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Waits a given milliseconds before resolving.
|
|
6
|
+
* @param time - The time to wait
|
|
7
|
+
*/
|
|
8
|
+
const wait = (time = 0): Promise<unknown> => new Promise((resolve) => setTimeout(resolve, time));
|
|
9
|
+
|
|
10
|
+
describe('lib/readinessManager', () => {
|
|
11
|
+
let readyManager: ReadinessManager;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
jest.isolateModules(() => {
|
|
15
|
+
readyManager = require('./index').default;
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(jest.clearAllMocks);
|
|
20
|
+
|
|
21
|
+
describe('API', () => {
|
|
22
|
+
it.each(['register', 'run', 'onReady', 'onActionReady', 'status', 'onError'])(
|
|
23
|
+
'Exposes public API (%p)',
|
|
24
|
+
(property) => {
|
|
25
|
+
expect(typeof (readyManager as any)[property]).toBe('function');
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
it.each(['beacons', 'callbacks', 'execute', 'updateBeacon'])(
|
|
30
|
+
'Encapsulates private properties (%p)',
|
|
31
|
+
(property) => {
|
|
32
|
+
expect((readyManager as any)[property]).toBeUndefined();
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('status', () => {
|
|
38
|
+
describe('When no actions are registered', () => {
|
|
39
|
+
it('Return an empty object', () => {
|
|
40
|
+
expect(readyManager.status()).toEqual({});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('When some actions exists', () => {
|
|
45
|
+
it('Group registered actions with same status', async () => {
|
|
46
|
+
readyManager.register('success', () => Promise.resolve());
|
|
47
|
+
readyManager.register('other', () => Promise.resolve());
|
|
48
|
+
readyManager.register('fail', () => Promise.reject(new Error()));
|
|
49
|
+
|
|
50
|
+
readyManager.run();
|
|
51
|
+
await wait(10);
|
|
52
|
+
|
|
53
|
+
const status = readyManager.status();
|
|
54
|
+
|
|
55
|
+
expect(status.resolved).toEqual(['success', 'other']);
|
|
56
|
+
expect(status.rejected).toEqual(['fail']);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('register', () => {
|
|
62
|
+
describe('When given action name is unique', () => {
|
|
63
|
+
it('Registers action and name as beacon with not_started status', () => {
|
|
64
|
+
readyManager.register('action', () => true);
|
|
65
|
+
|
|
66
|
+
expect(readyManager.status().not_started).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('When action name was already registered', () => {
|
|
71
|
+
it('Throws upon registration', () => {
|
|
72
|
+
readyManager.register('once', () => true);
|
|
73
|
+
|
|
74
|
+
expect(() => readyManager.register('once', () => true)).toThrowError(ERRORS.BEACON_ALREADY_EXISTS);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('run', () => {
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
readyManager.register('action1', wait);
|
|
82
|
+
readyManager.register('action2', wait);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('Executes all given actions', async () => {
|
|
86
|
+
expect(readyManager.status().not_started).toHaveLength(2);
|
|
87
|
+
|
|
88
|
+
readyManager.run();
|
|
89
|
+
|
|
90
|
+
expect(readyManager.status().not_started).toBeUndefined();
|
|
91
|
+
|
|
92
|
+
expect(readyManager.status().pending).toHaveLength(2);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('ready', () => {
|
|
97
|
+
it('Returns false by default', () => {
|
|
98
|
+
expect(readyManager.ready).toBeFalsy();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('When all actions resolves', () => {
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
readyManager.register('action1', () => true);
|
|
104
|
+
readyManager.register('action2', () => true);
|
|
105
|
+
|
|
106
|
+
readyManager.run();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('Is ready', () => expect(readyManager.ready).toBeTruthy());
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('When some action fails', () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
readyManager.register('action1', () => true);
|
|
115
|
+
readyManager.register('action2', () => {
|
|
116
|
+
throw new Error('What?');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
readyManager.run();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('Is not ready', () => expect(readyManager.ready).toBeFalsy());
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('onReady', () => {
|
|
127
|
+
let callback: jest.Mock;
|
|
128
|
+
|
|
129
|
+
beforeEach(() => (callback = jest.fn()));
|
|
130
|
+
|
|
131
|
+
const ensureFresh = () => {
|
|
132
|
+
readyManager.register('action1', () => true);
|
|
133
|
+
expect(callback).toHaveBeenCalledTimes(0);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
describe('When process is not ready', () => {
|
|
137
|
+
it('Registers given callback', () => {
|
|
138
|
+
const callback = jest.fn();
|
|
139
|
+
ensureFresh();
|
|
140
|
+
|
|
141
|
+
readyManager.onReady(callback);
|
|
142
|
+
readyManager.run();
|
|
143
|
+
|
|
144
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('When process is already ready', () => {
|
|
149
|
+
it('Activates immediately given callback', () => {
|
|
150
|
+
ensureFresh();
|
|
151
|
+
|
|
152
|
+
readyManager.run();
|
|
153
|
+
readyManager.onReady(callback);
|
|
154
|
+
|
|
155
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('onActionReady', () => {
|
|
161
|
+
let callback: jest.Mock;
|
|
162
|
+
|
|
163
|
+
beforeEach(() => (callback = jest.fn()));
|
|
164
|
+
|
|
165
|
+
it('Throws when action does not exists', () => {
|
|
166
|
+
expect(() => readyManager.onActionReady('Hey', () => false)).toThrowError(ERRORS.BEACON_DOES_NOT_EXISTS);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('When action is not ready', () => {
|
|
170
|
+
it('Triggers callback once desired aciton is ready', async () => {
|
|
171
|
+
readyManager.register('some', () => true);
|
|
172
|
+
readyManager.register('thing', () => wait(500));
|
|
173
|
+
readyManager.register('other', () => wait(1000));
|
|
174
|
+
|
|
175
|
+
readyManager.onActionReady('thing', callback);
|
|
176
|
+
|
|
177
|
+
readyManager.run();
|
|
178
|
+
expect(callback).toHaveBeenCalledTimes(0);
|
|
179
|
+
|
|
180
|
+
await wait(500);
|
|
181
|
+
|
|
182
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('When action is already resolved', () => {
|
|
187
|
+
it('Triggers immediately given callback', () => {
|
|
188
|
+
readyManager.register('some', () => true);
|
|
189
|
+
|
|
190
|
+
readyManager.run();
|
|
191
|
+
readyManager.onActionReady('some', callback);
|
|
192
|
+
|
|
193
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('onError', () => {
|
|
199
|
+
let logger: jest.Mock;
|
|
200
|
+
beforeEach(() => (logger = jest.fn()));
|
|
201
|
+
|
|
202
|
+
describe('When no errors occur', () => {
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
readyManager.register('success', () => true);
|
|
205
|
+
readyManager.onError((error) => {
|
|
206
|
+
logger(error);
|
|
207
|
+
});
|
|
208
|
+
readyManager.run();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("Won't trigger error handler", () => {
|
|
212
|
+
expect(logger).toHaveBeenCalledTimes(0);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('When error occur', () => {
|
|
217
|
+
const error = new Error('Oh man, that did not worked!');
|
|
218
|
+
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
readyManager.register('success', () => {
|
|
221
|
+
throw error;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
readyManager.onError((error) => {
|
|
225
|
+
logger(error);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
readyManager.run();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('Triggers consumer handler with beacon execution error', () => {
|
|
232
|
+
expect(logger).toHaveBeenCalledWith(new ActionExecutionError('success', 1, error));
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('Chainable', () => {
|
|
238
|
+
it('Expose public chainable api', () => {
|
|
239
|
+
const ready = jest.fn();
|
|
240
|
+
const error = jest.fn();
|
|
241
|
+
|
|
242
|
+
readyManager.register('action', () => true);
|
|
243
|
+
|
|
244
|
+
readyManager.run().onReady(ready).onError(error);
|
|
245
|
+
|
|
246
|
+
expect(ready).toHaveBeenCalledTimes(1);
|
|
247
|
+
expect(error).toHaveBeenCalledTimes(0);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A status of an action.
|
|
3
|
+
*/
|
|
4
|
+
export type ActionStatus = 'not_started' | 'pending' | 'resolved' | 'rejected';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A callback to be executed when an action is ready.
|
|
8
|
+
*/
|
|
9
|
+
export type ReadyCallback = () => void;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An action is a single function that needs to be executed to determine if the app is ready.
|
|
13
|
+
*/
|
|
14
|
+
export type ReadyAction = () => unknown | Promise<unknown>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A map of action statuses by their name.
|
|
18
|
+
*/
|
|
19
|
+
export type ActionsStatus = {
|
|
20
|
+
[key in ActionStatus]?: string[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A beacon is a single action that needs to be executed to determine if the app is ready.
|
|
25
|
+
*/
|
|
26
|
+
export interface Beacon {
|
|
27
|
+
name: string;
|
|
28
|
+
action: ReadyAction;
|
|
29
|
+
status: ActionStatus;
|
|
30
|
+
callbacks: ReadyCallback[];
|
|
31
|
+
debug?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A map of beacons by their name.
|
|
35
|
+
*/
|
|
36
|
+
export interface BeaconsMap {
|
|
37
|
+
[name: string]: Beacon;
|
|
38
|
+
}
|
package/tests/mock.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ReadinessManager } from '../src/index';
|
|
2
|
+
import type { ReadyCallback, ReadyAction, ActionsStatus } from '../src/types';
|
|
3
|
+
import type { ActionErrorHandler } from '../src/error';
|
|
4
|
+
|
|
5
|
+
export interface ReadinessManagerMock extends ReadinessManager {
|
|
6
|
+
setReady: (isReady: boolean) => void;
|
|
7
|
+
orchestrate: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Test mock helper class.
|
|
12
|
+
*/
|
|
13
|
+
class ReadinessManagerMockImpl implements ReadinessManagerMock {
|
|
14
|
+
private _ready = false;
|
|
15
|
+
|
|
16
|
+
get ready(): boolean {
|
|
17
|
+
return this._ready;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
status(): ActionsStatus {
|
|
21
|
+
return {} as ActionsStatus;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setReady(isReady: boolean): void {
|
|
25
|
+
this._ready = isReady;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
register(_name: string, _action: ReadyAction, _debug?: boolean): void {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
run(): ReadinessManager {
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
orchestrate(): void {
|
|
37
|
+
this._ready = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
onReady(callback: ReadyCallback): ReadinessManager {
|
|
41
|
+
callback();
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onActionReady(_name: string, callback: ReadyCallback): ReadinessManager {
|
|
46
|
+
callback();
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onError(_errorHandler: ActionErrorHandler): ReadinessManager {
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const manager: ReadinessManagerMock = new ReadinessManagerMockImpl();
|
|
56
|
+
|
|
57
|
+
export default manager;
|