@epam/uui-db 5.3.0-rc.1 → 5.3.0
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/benchmark/results/filter.json +39 -0
- package/benchmark/results/pkLookup.json +39 -0
- package/benchmark/results/pkUpdate-10.json +51 -0
- package/benchmark/results/pkUpdate-100.json +51 -0
- package/benchmark/results/pkUpdate-1000.json +51 -0
- package/benchmark/results/pkUpdate-10000.json +51 -0
- package/benchmark/results/pkUpdate-100000.json +51 -0
- package/benchmark/results/pkUpdate-1000000.json +51 -0
- package/benchmark/results/query-10.json +63 -0
- package/benchmark/results/query-100.json +63 -0
- package/benchmark/results/query-1000.json +63 -0
- package/benchmark/results/query-10000.json +63 -0
- package/benchmark/results/query-100000.json +63 -0
- package/benchmark/results/sorting.json +33 -0
- package/build/package.json +69 -0
- package/build/readme.md +17 -0
- package/{stats.html → build/stats.html} +1 -1
- package/globals.d.ts +9 -0
- package/index.tsx +1 -0
- package/package.json +5 -5
- package/src/Db.ts +89 -0
- package/src/DbRef.ts +254 -0
- package/src/DbTable.ts +264 -0
- package/src/IxSet.ts +251 -0
- package/src/ListLoadingTracker.ts +131 -0
- package/src/Loader.ts +96 -0
- package/src/Scheduler.ts +72 -0
- package/src/SimpleLoadingTracker.ts +17 -0
- package/src/__benchmarks__/filter.ts +50 -0
- package/src/__benchmarks__/index.ts +5 -0
- package/src/__benchmarks__/pkLookup.ts +33 -0
- package/src/__benchmarks__/pkUpdate.ts +75 -0
- package/src/__benchmarks__/query.ts +101 -0
- package/src/__benchmarks__/sorting.ts +33 -0
- package/src/__benchmarks__/testData.ts +15 -0
- package/src/__tests__/ListLoadingTracker.test.ts +83 -0
- package/src/__tests__/Scheduler.test.ts +24 -0
- package/src/__tests__/TaskDb.ts +187 -0
- package/src/__tests__/db.test.ts +293 -0
- package/src/__tests__/dbRef.test.ts +100 -0
- package/src/__tests__/dbTable.test.ts +15 -0
- package/src/__tests__/dbViews.test.ts +76 -0
- package/src/__tests__/ixSet.perf.test.ts +129 -0
- package/src/__tests__/ixSet.test.ts +73 -0
- package/src/__tests__/listLoader.test.ts +85 -0
- package/src/__tests__/loader.test.ts +92 -0
- package/src/__tests__/patchHelpers.test.ts +202 -0
- package/src/__tests__/tempIds.test.ts +98 -0
- package/src/index.ts +8 -0
- package/src/patchHelpers.ts +254 -0
- package/src/reactHooks.ts +28 -0
- package/src/tempIds.tsx +236 -0
- package/src/types.ts +126 -0
- package/src/utils.ts +34 -0
- package/tsconfig.json +12 -0
- /package/{index.d.ts → build/index.d.ts} +0 -0
- /package/{index.d.ts.map → build/index.d.ts.map} +0 -0
- /package/{index.esm.js → build/index.esm.js} +0 -0
- /package/{index.esm.js.map → build/index.esm.js.map} +0 -0
- /package/{index.js → build/index.js} +0 -0
- /package/{index.js.map → build/index.js.map} +0 -0
- /package/{src → build/src}/Db.d.ts +0 -0
- /package/{src → build/src}/Db.d.ts.map +0 -0
- /package/{src → build/src}/DbRef.d.ts +0 -0
- /package/{src → build/src}/DbRef.d.ts.map +0 -0
- /package/{src → build/src}/DbTable.d.ts +0 -0
- /package/{src → build/src}/DbTable.d.ts.map +0 -0
- /package/{src → build/src}/IxSet.d.ts +0 -0
- /package/{src → build/src}/IxSet.d.ts.map +0 -0
- /package/{src → build/src}/ListLoadingTracker.d.ts +0 -0
- /package/{src → build/src}/ListLoadingTracker.d.ts.map +0 -0
- /package/{src → build/src}/Loader.d.ts +0 -0
- /package/{src → build/src}/Loader.d.ts.map +0 -0
- /package/{src → build/src}/Scheduler.d.ts +0 -0
- /package/{src → build/src}/Scheduler.d.ts.map +0 -0
- /package/{src → build/src}/SimpleLoadingTracker.d.ts +0 -0
- /package/{src → build/src}/SimpleLoadingTracker.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/filter.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/filter.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/index.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/index.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/pkLookup.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/pkLookup.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/pkUpdate.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/pkUpdate.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/query.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/query.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/sorting.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/sorting.d.ts.map +0 -0
- /package/{src → build/src}/__benchmarks__/testData.d.ts +0 -0
- /package/{src → build/src}/__benchmarks__/testData.d.ts.map +0 -0
- /package/{src → build/src}/index.d.ts +0 -0
- /package/{src → build/src}/index.d.ts.map +0 -0
- /package/{src → build/src}/patchHelpers.d.ts +0 -0
- /package/{src → build/src}/patchHelpers.d.ts.map +0 -0
- /package/{src → build/src}/reactHooks.d.ts +0 -0
- /package/{src → build/src}/reactHooks.d.ts.map +0 -0
- /package/{src → build/src}/tempIds.d.ts +0 -0
- /package/{src → build/src}/tempIds.d.ts.map +0 -0
- /package/{src → build/src}/types.d.ts +0 -0
- /package/{src → build/src}/types.d.ts.map +0 -0
- /package/{src → build/src}/utils.d.ts +0 -0
- /package/{src → build/src}/utils.d.ts.map +0 -0
package/src/Loader.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { DbRef } from './DbRef';
|
|
2
|
+
import {
|
|
3
|
+
DbPatch, DbTablesSet, LoadingState, ILoadingTracker,
|
|
4
|
+
} from './types';
|
|
5
|
+
|
|
6
|
+
export interface LoaderOptions<TTables extends DbTablesSet<TTables>, TResult, TRequest> {
|
|
7
|
+
api: (request: TRequest) => Promise<TResult>;
|
|
8
|
+
convertToPatch?: (result: TResult) => DbPatch<TTables>;
|
|
9
|
+
postProcess?: (patch: DbPatch<TTables>) => DbPatch<TTables>;
|
|
10
|
+
clientToServerRequest?: (request: TRequest) => TRequest;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface LoaderResult<TRequest> extends LoadingState<TRequest> {
|
|
14
|
+
promise: Promise<void>;
|
|
15
|
+
reload: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Loader<TTables extends DbTablesSet<TTables>, TResult, TRequest> {
|
|
19
|
+
private currentRequests: Set<Promise<void>> = new Set();
|
|
20
|
+
public loaded: ILoadingTracker<TRequest, TResult>;
|
|
21
|
+
private loadedAndLoading: ILoadingTracker<TRequest, TResult>;
|
|
22
|
+
constructor(
|
|
23
|
+
private dbRef: DbRef<TTables, any>,
|
|
24
|
+
private getTracker: () => ILoadingTracker<TRequest, TResult>,
|
|
25
|
+
private options: LoaderOptions<TTables, TResult, TRequest>,
|
|
26
|
+
) {
|
|
27
|
+
this.loaded = getTracker();
|
|
28
|
+
this.loadedAndLoading = getTracker();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private loadImpl(state: LoaderResult<TRequest>) {
|
|
32
|
+
this.loadedAndLoading.append(state.missing);
|
|
33
|
+
state.isLoading = true;
|
|
34
|
+
let serverRequest = state.missing;
|
|
35
|
+
if (this.options.clientToServerRequest) {
|
|
36
|
+
serverRequest = this.options.clientToServerRequest(serverRequest);
|
|
37
|
+
}
|
|
38
|
+
state.promise = this.options.api(serverRequest).then((result) => {
|
|
39
|
+
this.loaded.append(state.missing, result);
|
|
40
|
+
this.loadedAndLoading.append(state.missing, result);
|
|
41
|
+
state.missing = null;
|
|
42
|
+
state.isLoading = false;
|
|
43
|
+
state.isComplete = true;
|
|
44
|
+
state.promise = Promise.resolve();
|
|
45
|
+
const flatten = this.options.convertToPatch(result);
|
|
46
|
+
this.options.postProcess && this.options.postProcess(flatten);
|
|
47
|
+
this.dbRef.commitFetch(flatten);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.currentRequests.add(state.promise);
|
|
51
|
+
|
|
52
|
+
state.promise.finally(() => {
|
|
53
|
+
this.currentRequests.delete(state.promise);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return state.promise;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public load = (inputRequest: TRequest) => {
|
|
60
|
+
const result: LoaderResult<TRequest> = {
|
|
61
|
+
isComplete: this.loaded.diff(inputRequest) == null,
|
|
62
|
+
isLoading: false,
|
|
63
|
+
request: inputRequest,
|
|
64
|
+
missing: this.loadedAndLoading.diff(inputRequest),
|
|
65
|
+
promise: null,
|
|
66
|
+
reload: null,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
result.reload = () => {
|
|
70
|
+
result.missing = inputRequest;
|
|
71
|
+
this.loadImpl(result);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (result.isComplete) {
|
|
75
|
+
// Everything requested is already loaded
|
|
76
|
+
result.isComplete = true;
|
|
77
|
+
result.isLoading = false;
|
|
78
|
+
result.promise = Promise.resolve();
|
|
79
|
+
return result;
|
|
80
|
+
} else {
|
|
81
|
+
if (result.missing == null) {
|
|
82
|
+
// Everything requested is either loaded already, or being loaded
|
|
83
|
+
result.isComplete = false;
|
|
84
|
+
result.isLoading = true;
|
|
85
|
+
// There's no good way to tell which of current requests is querying requested data - there actually can be 2 of them.
|
|
86
|
+
// So we track all running requests, and await all of them in such case.
|
|
87
|
+
result.promise = Promise.all(this.currentRequests.entries()).then(() => {});
|
|
88
|
+
return result;
|
|
89
|
+
} else {
|
|
90
|
+
// Something requested is not loaded, nor being loaded. Need to perform a new request.
|
|
91
|
+
this.loadImpl(result);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
package/src/Scheduler.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export interface SchedulerTask {
|
|
2
|
+
run: () => Promise<any>;
|
|
3
|
+
result?: any;
|
|
4
|
+
error?: any;
|
|
5
|
+
resolve?: (result: any) => void;
|
|
6
|
+
reject?: (result: any) => void;
|
|
7
|
+
isConcurrent: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class Scheduler {
|
|
11
|
+
scheduled: SchedulerTask[] = [];
|
|
12
|
+
running: SchedulerTask[] = [];
|
|
13
|
+
complete: SchedulerTask[] = [];
|
|
14
|
+
public run(run: () => Promise<any>, isConcurrent = false) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
this.scheduled.push({
|
|
17
|
+
run, isConcurrent, resolve, reject,
|
|
18
|
+
});
|
|
19
|
+
this.scheduleRun();
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
isRunScheduled = false;
|
|
24
|
+
private scheduleRun() {
|
|
25
|
+
if (this.isRunScheduled) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
this.isRunScheduled = false;
|
|
30
|
+
this.runQueues();
|
|
31
|
+
}, 0);
|
|
32
|
+
this.isRunScheduled = true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
tasksRunning = 0;
|
|
36
|
+
private runQueues() {
|
|
37
|
+
while (this.scheduled.length > 0) {
|
|
38
|
+
const pendingTask = this.scheduled[0];
|
|
39
|
+
if (this.running.length > 0 && !pendingTask.isConcurrent) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this.running.length > 0 && !this.running[0].isConcurrent) {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const task = this.scheduled.shift();
|
|
48
|
+
this.running.push(task);
|
|
49
|
+
task.run()
|
|
50
|
+
.then((r) => {
|
|
51
|
+
task.result = r;
|
|
52
|
+
})
|
|
53
|
+
.catch((err) => {
|
|
54
|
+
task.error = err;
|
|
55
|
+
})
|
|
56
|
+
.finally(() => {
|
|
57
|
+
this.complete.push(task);
|
|
58
|
+
this.running = this.running.filter((t) => t !== task);
|
|
59
|
+
this.scheduleRun();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
while (this.complete.length > 0) {
|
|
64
|
+
const task = this.complete.shift();
|
|
65
|
+
if (task.error) {
|
|
66
|
+
task.reject(task.error);
|
|
67
|
+
} else {
|
|
68
|
+
task.resolve(task.result);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ILoadingTracker } from './types';
|
|
2
|
+
|
|
3
|
+
export class SimpleLoadingTracker<TRequest, TResult> implements ILoadingTracker<TRequest, TResult> {
|
|
4
|
+
private set: Set<string> = new Set();
|
|
5
|
+
constructor() {}
|
|
6
|
+
private getKey(request: TRequest) {
|
|
7
|
+
return JSON.stringify(request);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public diff(request: TRequest) {
|
|
11
|
+
return this.set.has(this.getKey(request)) ? null : request;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public append(request: TRequest, result: TResult) {
|
|
15
|
+
this.set.add(this.getKey(request));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as b from 'benny';
|
|
2
|
+
import { Person } from './testData';
|
|
3
|
+
import range from 'lodash.range';
|
|
4
|
+
import { getFilterPredicate } from '@epam/uui-core';
|
|
5
|
+
|
|
6
|
+
const testPersons = range(0, 1e4).map((id) => ({ id, name: `Person ${id}`, departmentId: Math.floor(Math.random() * 100) }));
|
|
7
|
+
|
|
8
|
+
b.suite(
|
|
9
|
+
'Filter array',
|
|
10
|
+
|
|
11
|
+
b.add('array.filter, hard-coded', () => {
|
|
12
|
+
return () => testPersons.filter((p) => p.departmentId == 5);
|
|
13
|
+
}),
|
|
14
|
+
|
|
15
|
+
b.add('for-loop, array.push, hard-coded', () => {
|
|
16
|
+
return () => {
|
|
17
|
+
const result = [];
|
|
18
|
+
for (let n = 0; n < testPersons.length; n++) {
|
|
19
|
+
const person = testPersons[n];
|
|
20
|
+
if (person.departmentId == 5) {
|
|
21
|
+
result.push(person);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}),
|
|
26
|
+
|
|
27
|
+
b.add('array.filter, getPatternPredicate', () => {
|
|
28
|
+
const predicate = getFilterPredicate<Person>({ departmentId: 5 });
|
|
29
|
+
return () => {
|
|
30
|
+
testPersons.filter(predicate);
|
|
31
|
+
};
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
b.add('for-loop, array.push, getPatternPredicate', () => {
|
|
35
|
+
const predicate = getFilterPredicate<Person>({ departmentId: 5 });
|
|
36
|
+
return () => {
|
|
37
|
+
const result = [];
|
|
38
|
+
for (let n = 0; n < testPersons.length; n++) {
|
|
39
|
+
const person = testPersons[n];
|
|
40
|
+
if (predicate(person)) {
|
|
41
|
+
result.push(person);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
b.cycle(),
|
|
48
|
+
b.complete(),
|
|
49
|
+
b.save({ file: 'filter', version: '1.0.0' }),
|
|
50
|
+
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as b from 'benny';
|
|
2
|
+
import { test100KPersons, blankIxSet } from './testData';
|
|
3
|
+
import * as I from 'immutable';
|
|
4
|
+
|
|
5
|
+
b.suite(
|
|
6
|
+
'Find 1 entity by number ID in 100k dataset',
|
|
7
|
+
|
|
8
|
+
// b.add('Array.filter', () => test100KPersons.find(p => p.id == 5)),
|
|
9
|
+
|
|
10
|
+
b.add('IxSet.byId', () => {
|
|
11
|
+
const set = blankIxSet.with(test100KPersons);
|
|
12
|
+
return () => set.byId(5);
|
|
13
|
+
}),
|
|
14
|
+
|
|
15
|
+
b.add('Map.get', () => {
|
|
16
|
+
const map = new Map(test100KPersons.map((p) => [p.id, p]));
|
|
17
|
+
return () => map.get(5);
|
|
18
|
+
}),
|
|
19
|
+
|
|
20
|
+
b.add('hash[id]', () => {
|
|
21
|
+
const map = (Object as any).fromEntries(test100KPersons.map((p) => [p.id, p]));
|
|
22
|
+
return () => map[5];
|
|
23
|
+
}),
|
|
24
|
+
|
|
25
|
+
b.add('I.Map.get(id)', () => {
|
|
26
|
+
const map = I.Map(test100KPersons.map((p) => [p.id, p]));
|
|
27
|
+
return () => map.get(5);
|
|
28
|
+
}),
|
|
29
|
+
|
|
30
|
+
b.cycle(),
|
|
31
|
+
b.complete(),
|
|
32
|
+
b.save({ file: 'pkLookup', version: '1.0.0' }),
|
|
33
|
+
);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as b from 'benny';
|
|
2
|
+
import * as I from 'immutable';
|
|
3
|
+
import BTree from 'sorted-btree';
|
|
4
|
+
import { Person, blankIxSet, blankIxSetNoIndex } from './testData';
|
|
5
|
+
import range from 'lodash.range';
|
|
6
|
+
import { DbTable } from '..';
|
|
7
|
+
|
|
8
|
+
[
|
|
9
|
+
1e1, 1e2, 1e3, 1e4, 1e5, 1e6,
|
|
10
|
+
].forEach((size) => {
|
|
11
|
+
const testPersons = range(0, size).map((id) => ({ id, name: `Person ${id}`, departmentId: Math.floor(Math.random() * 10) }));
|
|
12
|
+
const pairs = testPersons.map((p) => [p.id, p] as [number, Person]);
|
|
13
|
+
const person = testPersons[0];
|
|
14
|
+
|
|
15
|
+
const personTableNoIndex = new DbTable<any, any, any>({
|
|
16
|
+
primaryKey: ['id'],
|
|
17
|
+
tableName: 'persons',
|
|
18
|
+
typeName: 'person',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const personTableWithIndex = new DbTable<any, any, any>({
|
|
22
|
+
primaryKey: ['id'],
|
|
23
|
+
tableName: 'persons',
|
|
24
|
+
typeName: 'person',
|
|
25
|
+
indexes: ['departmentId'],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
b.suite(
|
|
29
|
+
`Update 1 entity in ${size} dataset`,
|
|
30
|
+
|
|
31
|
+
b.add('BTree - with()', () => {
|
|
32
|
+
const set = new BTree(pairs);
|
|
33
|
+
return () => set.with(5, person);
|
|
34
|
+
}),
|
|
35
|
+
|
|
36
|
+
b.add('Map - clone with Map(previous)', () => {
|
|
37
|
+
const map = new Map(pairs);
|
|
38
|
+
return () => {
|
|
39
|
+
const m = new Map(map);
|
|
40
|
+
m.set(5, person);
|
|
41
|
+
};
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
b.add('hash - spread clone', () => {
|
|
45
|
+
const map = (Object as any).fromEntries(pairs);
|
|
46
|
+
return () => {
|
|
47
|
+
const newMap = { ...map, 5: person };
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
b.add('I.Map.set(id)', () => {
|
|
52
|
+
const map = I.Map(pairs);
|
|
53
|
+
return () => map.set(5, person);
|
|
54
|
+
}),
|
|
55
|
+
|
|
56
|
+
b.add('DbTable - no index', () => {
|
|
57
|
+
const table = personTableNoIndex.with(testPersons);
|
|
58
|
+
return () => table.with([person]);
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
b.add('DbTable - single index', () => {
|
|
62
|
+
const table = personTableWithIndex.with(testPersons);
|
|
63
|
+
return () => table.with([person]);
|
|
64
|
+
}),
|
|
65
|
+
|
|
66
|
+
b.add('IxSet (with two indexes)', () => {
|
|
67
|
+
const set = blankIxSet.with(testPersons);
|
|
68
|
+
return () => set.with([person]);
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
b.cycle(),
|
|
72
|
+
b.complete(),
|
|
73
|
+
b.save({ file: `pkUpdate-${size}`, version: '1.0.0' }),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import * as b from 'benny';
|
|
2
|
+
import * as I from 'immutable';
|
|
3
|
+
import BTree from 'sorted-btree';
|
|
4
|
+
import orderBy from 'lodash.orderby';
|
|
5
|
+
import sortBy from 'lodash.sortby';
|
|
6
|
+
import range from 'lodash.range';
|
|
7
|
+
import { Person, blankIxSet, blankIxSetNoIndex } from './testData';
|
|
8
|
+
import { getFilterPredicate, getOrderComparer } from '@epam/uui-core';
|
|
9
|
+
import { DbTable } from '..';
|
|
10
|
+
|
|
11
|
+
[
|
|
12
|
+
1e1, 1e2, 1e3, 1e4, 1e5,
|
|
13
|
+
].forEach((size) => {
|
|
14
|
+
const testPersons = range(0, size).map((id) => ({ id, name: `Person ${id}`, departmentId: Math.floor((Math.random() * size) / 10) }));
|
|
15
|
+
const pairs = testPersons.map((p) => [p.id, p] as [number, Person]);
|
|
16
|
+
const filterPredicate = getFilterPredicate<Person>({ departmentId: 5 });
|
|
17
|
+
const orderComparer = getOrderComparer<Person>([{ field: 'name' }]);
|
|
18
|
+
|
|
19
|
+
const personTableNoIndex = new DbTable<any, any, any>({
|
|
20
|
+
primaryKey: ['id'],
|
|
21
|
+
tableName: 'persons',
|
|
22
|
+
typeName: 'person',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const personTableWithIndex = new DbTable<any, any, any>({
|
|
26
|
+
primaryKey: ['id'],
|
|
27
|
+
tableName: 'persons',
|
|
28
|
+
typeName: 'person',
|
|
29
|
+
indexes: ['departmentId'],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
b.suite(
|
|
33
|
+
`Find N entities in ${size} dataset by departmentId, sorted by name`,
|
|
34
|
+
|
|
35
|
+
b.add('Array - scan', () => {
|
|
36
|
+
return () =>
|
|
37
|
+
orderBy(
|
|
38
|
+
testPersons.filter((p) => p.departmentId == 5),
|
|
39
|
+
'name',
|
|
40
|
+
);
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
b.add('Array - with filterPredicate/orderComparer', () => {
|
|
44
|
+
return () => testPersons.filter(filterPredicate).sort(orderComparer);
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
b.add('BTree - toArray/scan', () => {
|
|
48
|
+
const set = new BTree(pairs);
|
|
49
|
+
return () => set.valuesArray().filter(filterPredicate).sort(orderComparer);
|
|
50
|
+
}),
|
|
51
|
+
|
|
52
|
+
b.add('I.Map - iterable.filter.sort, then toArray', () => {
|
|
53
|
+
const set = I.Map(pairs);
|
|
54
|
+
return () => (set as I.Iterable<number, Person>).filter(filterPredicate).sort(orderComparer).toArray();
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
b.add('I.Map - toArray, then filter/sort', () => {
|
|
58
|
+
const set = I.Map(pairs);
|
|
59
|
+
return () => sortBy((set as I.Iterable<number, Person>).toArray().filter(filterPredicate), 'name');
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
// Super-fast by design, probably the best way to go with indexes.
|
|
63
|
+
// Skip for now to compare others.
|
|
64
|
+
//
|
|
65
|
+
// b.add('Lookup by I.Map with BTrees', () => {
|
|
66
|
+
// const byDepId = I.Map<number, BTree<string, Person>>();
|
|
67
|
+
// testPersons.forEach((p) => {
|
|
68
|
+
// let bTree = byDepId.get(p.departmentId);
|
|
69
|
+
// if (!bTree) {
|
|
70
|
+
// bTree = new BTree<string, Person>();
|
|
71
|
+
// byDepId.set(p.departmentId, bTree);
|
|
72
|
+
// }
|
|
73
|
+
// bTree.set(p.name, p);
|
|
74
|
+
// });
|
|
75
|
+
// const set = new BTree(pairs);
|
|
76
|
+
// return () => byDepId.get(5);
|
|
77
|
+
// }),
|
|
78
|
+
|
|
79
|
+
b.add('IxSet - with indexes', () => {
|
|
80
|
+
const set = blankIxSet.with(testPersons);
|
|
81
|
+
return () => set.query({ filter: { departmentId: 5 }, sorting: [{ field: 'name' }] });
|
|
82
|
+
}),
|
|
83
|
+
|
|
84
|
+
b.add('IxSet - no indexes', () => {
|
|
85
|
+
const set = blankIxSetNoIndex.with(testPersons);
|
|
86
|
+
return () => set.query({ filter: { departmentId: 5 }, sorting: [{ field: 'name' }] });
|
|
87
|
+
}),
|
|
88
|
+
|
|
89
|
+
b.add('DbTable - no index', () => {
|
|
90
|
+
return () => personTableNoIndex.find({ departmentId: 5 }).orderBy('name');
|
|
91
|
+
}),
|
|
92
|
+
|
|
93
|
+
b.add('DbTable - with index', () => {
|
|
94
|
+
return () => personTableWithIndex.find({ departmentId: 5 }).orderBy('name');
|
|
95
|
+
}),
|
|
96
|
+
|
|
97
|
+
b.cycle(),
|
|
98
|
+
b.complete(),
|
|
99
|
+
b.save({ file: `query-${size}`, version: '1.0.0' }),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as b from 'benny';
|
|
2
|
+
import orderBy from 'lodash.orderby';
|
|
3
|
+
import { Person } from './testData';
|
|
4
|
+
import { getFilterPredicate, getOrderComparer } from '@epam/uui-core';
|
|
5
|
+
import range from 'lodash.range';
|
|
6
|
+
|
|
7
|
+
const testPersons = range(0, 1e4).map((id) => ({ id, name: `Person ${id}`, departmentId: Math.floor(Math.random() * 100) }));
|
|
8
|
+
|
|
9
|
+
const nameComparer = (p1: Person, p2: Person) => (p1.name > p2.name ? 1 : -1);
|
|
10
|
+
|
|
11
|
+
b.suite(
|
|
12
|
+
'Sort array',
|
|
13
|
+
|
|
14
|
+
b.add('array.sort, hard-coded', () => {
|
|
15
|
+
const arr = [...testPersons];
|
|
16
|
+
return () => arr.sort(nameComparer);
|
|
17
|
+
}),
|
|
18
|
+
|
|
19
|
+
b.add('_.sort, hard-coded', () => {
|
|
20
|
+
const arr = [...testPersons];
|
|
21
|
+
return () => orderBy(arr, 'name');
|
|
22
|
+
}),
|
|
23
|
+
|
|
24
|
+
b.add('array.sort, getOrderComparer', () => {
|
|
25
|
+
const arr = [...testPersons];
|
|
26
|
+
const comparer = getOrderComparer<Person>([{ field: 'name' }]);
|
|
27
|
+
return () => arr.sort(comparer);
|
|
28
|
+
}),
|
|
29
|
+
|
|
30
|
+
b.cycle(),
|
|
31
|
+
b.complete(),
|
|
32
|
+
b.save({ file: 'sorting', version: '1.0.0' }),
|
|
33
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import range from 'lodash.range';
|
|
2
|
+
import { IxSet } from '../IxSet';
|
|
3
|
+
|
|
4
|
+
export interface Person {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
departmentId: number;
|
|
8
|
+
location?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const test100KPersons = range(101000, 999, -1).map((id) => ({ id, name: `Person ${id}`, departmentId: Math.floor(Math.random() * 10) }));
|
|
12
|
+
|
|
13
|
+
export const blankIxSet = new IxSet<Person, number>((i) => i.id, [{ fields: ['name'] }, { fields: ['departmentId', 'name'] }]);
|
|
14
|
+
|
|
15
|
+
export const blankIxSetNoIndex = new IxSet<Person, number>((i) => i.id, []);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Task } from './TaskDb';
|
|
2
|
+
import { ListLoadingTracker } from '../ListLoadingTracker';
|
|
3
|
+
import { DataQuery, LazyDataSourceApiResponse, runDataQuery } from '@epam/uui-core';
|
|
4
|
+
import range from 'lodash.range';
|
|
5
|
+
|
|
6
|
+
describe('db - ListLoadingTracker', () => {
|
|
7
|
+
const testItems = range(200).map((id) => {
|
|
8
|
+
return {
|
|
9
|
+
id, name: 'Task', isDone: false, createdBy: 'JS', isDraft: false, assignedTo: 'DT',
|
|
10
|
+
} as Task;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function getMockResponse(request: DataQuery<Task>): LazyDataSourceApiResponse<Task> {
|
|
14
|
+
return runDataQuery(testItems, request);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
it('Should correctly process single ranged load', () => {
|
|
18
|
+
const tracker = new ListLoadingTracker<Task>();
|
|
19
|
+
|
|
20
|
+
const request = { filter: { assignedTo: 'DT' }, range: { from: 0, count: 10 } };
|
|
21
|
+
const request1 = tracker.diff(request);
|
|
22
|
+
expect(request1).toEqual(request);
|
|
23
|
+
|
|
24
|
+
tracker.append(request);
|
|
25
|
+
|
|
26
|
+
const request2 = tracker.diff(request);
|
|
27
|
+
expect(request2).toEqual(null);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('Should correctly process overlapping ranges', () => {
|
|
31
|
+
const tracker = new ListLoadingTracker<Task>();
|
|
32
|
+
|
|
33
|
+
const request = { range: { from: 0, count: 10 } };
|
|
34
|
+
tracker.append(request, getMockResponse(request));
|
|
35
|
+
|
|
36
|
+
const overlappingRequest = { range: { from: 5, count: 10 } };
|
|
37
|
+
const diffRequest = tracker.diff(overlappingRequest);
|
|
38
|
+
expect(diffRequest).toEqual({ range: { from: 10, count: 5 } });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('Should correctly process non-overlapping ranges', () => {
|
|
42
|
+
const tracker = new ListLoadingTracker<Task>();
|
|
43
|
+
|
|
44
|
+
const request = { range: { from: 0, count: 10 } };
|
|
45
|
+
tracker.append(request, getMockResponse(request));
|
|
46
|
+
|
|
47
|
+
const nonOverlappingRequest = { range: { from: 20, count: 10 } };
|
|
48
|
+
const missingRequest = tracker.diff(nonOverlappingRequest);
|
|
49
|
+
|
|
50
|
+
expect(missingRequest).toEqual({ range: { from: 10, count: 20 } });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('Handle complete list correctly (after explicit load with no range)', () => {
|
|
54
|
+
const tracker = new ListLoadingTracker<Task>();
|
|
55
|
+
|
|
56
|
+
const request = {};
|
|
57
|
+
const loadRequest = tracker.diff(request);
|
|
58
|
+
expect(loadRequest).toEqual(request);
|
|
59
|
+
|
|
60
|
+
tracker.append(request);
|
|
61
|
+
|
|
62
|
+
const loadRequest2 = tracker.diff({});
|
|
63
|
+
expect(loadRequest2).toEqual(null);
|
|
64
|
+
|
|
65
|
+
const loadRequest3 = tracker.diff({ range: { from: 20, count: 10 } });
|
|
66
|
+
expect(loadRequest3).toEqual(null);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('Handle complete list correctly (with implicit end of list detection)', () => {
|
|
70
|
+
const tracker = new ListLoadingTracker<Task>();
|
|
71
|
+
|
|
72
|
+
const request = { range: { from: 0, count: 2000 } };
|
|
73
|
+
const loadRequest = tracker.diff(request);
|
|
74
|
+
expect(loadRequest).toEqual(request);
|
|
75
|
+
tracker.append(request, getMockResponse(request));
|
|
76
|
+
|
|
77
|
+
const loadRequest2 = tracker.diff({});
|
|
78
|
+
expect(loadRequest2).toEqual(null);
|
|
79
|
+
|
|
80
|
+
const loadRequest3 = tracker.diff({ range: { from: 20, count: 10 } });
|
|
81
|
+
expect(loadRequest2).toEqual(null);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Scheduler } from '../Scheduler';
|
|
2
|
+
|
|
3
|
+
const delay = (t: number) =>
|
|
4
|
+
new Promise((resolve) => {
|
|
5
|
+
setTimeout(resolve, t);
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const testApi = async (params: any) => {
|
|
9
|
+
await delay(1);
|
|
10
|
+
return params;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('db - Scheduler', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
// testTask.mockReset();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('runs and completes a single task', async () => {
|
|
19
|
+
const scheduler = new Scheduler();
|
|
20
|
+
const testTask = jest.fn(testApi);
|
|
21
|
+
const result = await scheduler.run(() => testTask(123));
|
|
22
|
+
expect(result).toBe(123);
|
|
23
|
+
});
|
|
24
|
+
});
|