@uwdata/mosaic-core 0.0.1 → 0.1.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/LICENSE +28 -0
- package/README.md +2 -2
- package/dist/mosaic-core.js +337 -226
- package/dist/mosaic-core.min.js +7 -7
- package/package.json +4 -3
- package/src/Catalog.js +24 -38
- package/src/Coordinator.js +32 -32
- package/src/DataTileIndexer.js +9 -7
- package/src/FilterGroup.js +3 -3
- package/src/MosaicClient.js +64 -0
- package/src/{Signal.js → Param.js} +11 -5
- package/src/QueryCache.js +13 -13
- package/src/Selection.js +2 -2
- package/src/index.js +2 -1
- package/src/util/distinct.js +14 -0
- package/src/util/summarize.js +23 -0
- package/src/util/throttle.js +22 -4
- package/src/util/void-logger.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uwdata/mosaic-core",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Scalable and extensible linked data views.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mosaic",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@duckdb/duckdb-wasm": "^1.20.0",
|
|
32
|
-
"@uwdata/mosaic-sql": "^0.0
|
|
32
|
+
"@uwdata/mosaic-sql": "^0.1.0",
|
|
33
33
|
"apache-arrow": "^11.0.0"
|
|
34
|
-
}
|
|
34
|
+
},
|
|
35
|
+
"gitHead": "a7967c35349bdf7f00abb113ce1dd9abb233cd62"
|
|
35
36
|
}
|
package/src/Catalog.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import { Query, count, max, min, isNull } from '@uwdata/mosaic-sql';
|
|
2
1
|
import { jsType } from './util/js-type.js';
|
|
2
|
+
import { summarize } from './util/summarize.js';
|
|
3
3
|
|
|
4
4
|
const object = () => Object.create(null);
|
|
5
5
|
|
|
6
6
|
export class Catalog {
|
|
7
|
-
constructor(
|
|
8
|
-
this.mc =
|
|
7
|
+
constructor(coordinator) {
|
|
8
|
+
this.mc = coordinator;
|
|
9
9
|
this.clear();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
clear() {
|
|
13
13
|
this.tables = object();
|
|
14
|
-
this.fields = object();
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
async tableInfo(table) {
|
|
@@ -21,62 +20,49 @@ export class Catalog {
|
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
const q = this.mc.query(
|
|
24
|
-
`
|
|
25
|
-
{ type: 'json' }
|
|
23
|
+
`DESCRIBE "${table}"`,
|
|
24
|
+
{ type: 'json', cache: false }
|
|
26
25
|
);
|
|
27
26
|
|
|
28
27
|
return (cache[table] = q.then(result => {
|
|
29
28
|
const columns = object();
|
|
30
29
|
for (const entry of result) {
|
|
31
|
-
columns[entry.
|
|
30
|
+
columns[entry.column_name] = {
|
|
31
|
+
table,
|
|
32
|
+
column: entry.column_name,
|
|
33
|
+
sqlType: entry.column_type,
|
|
34
|
+
type: jsType(entry.column_type),
|
|
35
|
+
nullable: entry.null === 'YES'
|
|
36
|
+
};
|
|
32
37
|
}
|
|
33
38
|
return columns;
|
|
34
39
|
}));
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
async fieldInfo(table, column) {
|
|
38
|
-
const
|
|
39
|
-
const colInfo =
|
|
42
|
+
async fieldInfo({ table, column, stats }) {
|
|
43
|
+
const tableInfo = await this.tableInfo(table);
|
|
44
|
+
const colInfo = tableInfo[column];
|
|
40
45
|
|
|
41
46
|
// column does not exist
|
|
42
47
|
if (colInfo == null) return;
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (cache[key]) {
|
|
47
|
-
return cache[key];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const promise = this.mc.query(
|
|
51
|
-
Query.from(table).select({
|
|
52
|
-
rows: count(),
|
|
53
|
-
nulls: count().where(isNull(column)),
|
|
54
|
-
min: min(column),
|
|
55
|
-
max: max(column)
|
|
56
|
-
}, { cache: false })
|
|
57
|
-
).then(result => {
|
|
58
|
-
const [ stats ] = Array.from(result);
|
|
59
|
-
return { table, column, type: colInfo.jstype, ...stats };
|
|
60
|
-
});
|
|
49
|
+
// no need for summary statistics
|
|
50
|
+
if (!stats?.length) return colInfo;
|
|
61
51
|
|
|
62
|
-
|
|
52
|
+
const result = await this.mc.query(summarize(colInfo, stats));
|
|
53
|
+
const info = { ...colInfo, ...(Array.from(result)[0]) };
|
|
54
|
+
return info;
|
|
63
55
|
}
|
|
64
56
|
|
|
65
57
|
async queryFields(fields) {
|
|
66
58
|
const list = await resolveFields(this, fields);
|
|
67
|
-
const data = await Promise.all(
|
|
68
|
-
list.map(f => this.fieldInfo(f.table, f.column))
|
|
69
|
-
)
|
|
59
|
+
const data = await Promise.all(list.map(f => this.fieldInfo(f)));
|
|
70
60
|
return data.filter(x => x);
|
|
71
61
|
}
|
|
72
62
|
}
|
|
73
63
|
|
|
74
64
|
async function resolveFields(catalog, list) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return Object.keys(info).map(column => ({ table, column }));
|
|
79
|
-
} else {
|
|
80
|
-
return list;
|
|
81
|
-
}
|
|
65
|
+
return list.length === 1 && list[0].column === '*'
|
|
66
|
+
? Object.values(await catalog.tableInfo(list[0].table))
|
|
67
|
+
: list;
|
|
82
68
|
}
|
package/src/Coordinator.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { socketClient } from './clients/socket.js';
|
|
2
2
|
import { Catalog } from './Catalog.js';
|
|
3
3
|
import { FilterGroup } from './FilterGroup.js';
|
|
4
|
-
import { QueryCache } from './QueryCache.js';
|
|
4
|
+
import { QueryCache, voidCache } from './QueryCache.js';
|
|
5
|
+
import { voidLogger } from './util/void-logger.js';
|
|
5
6
|
|
|
6
7
|
let _instance;
|
|
7
8
|
|
|
@@ -15,28 +16,30 @@ export function coordinator(instance) {
|
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export class Coordinator {
|
|
18
|
-
constructor(db = socketClient()) {
|
|
19
|
-
this.cache = new QueryCache();
|
|
19
|
+
constructor(db = socketClient(), options = {}) {
|
|
20
20
|
this.catalog = new Catalog(this);
|
|
21
|
-
this.
|
|
21
|
+
this.logger(options.logger || console);
|
|
22
|
+
this.configure(options);
|
|
22
23
|
this.databaseClient(db);
|
|
23
24
|
this.clear();
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
logger(logger) {
|
|
28
|
+
return arguments.length
|
|
29
|
+
? (this._logger = logger || voidLogger())
|
|
30
|
+
: this._logger;
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
configure({ cache = true, indexes = true }) {
|
|
27
|
-
this.cache = cache ? new QueryCache() :
|
|
28
|
-
get: () => undefined,
|
|
29
|
-
set: (key, result) => result,
|
|
30
|
-
clear: () => {}
|
|
31
|
-
};
|
|
34
|
+
this.cache = cache ? new QueryCache() : voidCache();
|
|
32
35
|
this.indexes = indexes;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
clear({ clients = true, cache = true, catalog = false } = {}) {
|
|
36
39
|
if (clients) {
|
|
37
|
-
this.clients?.forEach(
|
|
40
|
+
this.clients?.forEach(client => this.disconnect(client));
|
|
38
41
|
this.filterGroups?.forEach(group => group.finalize());
|
|
39
|
-
this.clients = new
|
|
42
|
+
this.clients = new Set;
|
|
40
43
|
this.filterGroups = new Map;
|
|
41
44
|
}
|
|
42
45
|
if (cache) this.cache.clear();
|
|
@@ -54,18 +57,22 @@ export class Coordinator {
|
|
|
54
57
|
try {
|
|
55
58
|
await this.db.query({ type: 'exec', sql });
|
|
56
59
|
} catch (err) {
|
|
57
|
-
|
|
60
|
+
this._logger.error(err);
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
|
|
64
|
+
query(query, { type = 'arrow', cache = true } = {}) {
|
|
62
65
|
const sql = String(query);
|
|
66
|
+
const t0 = performance.now();
|
|
63
67
|
const cached = this.cache.get(sql);
|
|
64
68
|
if (cached) {
|
|
69
|
+
this._logger.debug('Cache');
|
|
65
70
|
return cached;
|
|
66
71
|
} else {
|
|
67
72
|
const request = this.db.query({ type, sql });
|
|
68
|
-
|
|
73
|
+
const result = cache ? this.cache.set(sql, request) : request;
|
|
74
|
+
result.then(() => this._logger.debug(`Query: ${performance.now() - t0}`));
|
|
75
|
+
return result;
|
|
69
76
|
}
|
|
70
77
|
}
|
|
71
78
|
|
|
@@ -75,24 +82,31 @@ export class Coordinator {
|
|
|
75
82
|
client.queryPending();
|
|
76
83
|
result = await this.query(query);
|
|
77
84
|
} catch (err) {
|
|
78
|
-
|
|
85
|
+
this._logger.error(err);
|
|
79
86
|
client.queryError(err);
|
|
80
87
|
return;
|
|
81
88
|
}
|
|
82
89
|
try {
|
|
83
90
|
client.queryResult(result).update();
|
|
84
91
|
} catch (err) {
|
|
85
|
-
|
|
92
|
+
this._logger.error(err);
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
|
|
96
|
+
async requestQuery(client, query) {
|
|
97
|
+
this.filterGroups.get(client.filterBy)?.reset();
|
|
98
|
+
return query
|
|
99
|
+
? this.updateClient(client, query)
|
|
100
|
+
: client.update();
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
async connect(client) {
|
|
90
104
|
const { catalog, clients, filterGroups, indexes } = this;
|
|
91
105
|
|
|
92
106
|
if (clients.has(client)) {
|
|
93
107
|
throw new Error('Client already connected.');
|
|
94
108
|
}
|
|
95
|
-
clients.
|
|
109
|
+
clients.add(client); // mark as connected
|
|
96
110
|
|
|
97
111
|
// retrieve field statistics
|
|
98
112
|
const fields = client.fields();
|
|
@@ -111,27 +125,13 @@ export class Coordinator {
|
|
|
111
125
|
}
|
|
112
126
|
}
|
|
113
127
|
|
|
114
|
-
|
|
115
|
-
const handler = async (query) => {
|
|
116
|
-
const q = query || client.query(filter?.predicate(client));
|
|
117
|
-
filterGroups.get(filter)?.reset();
|
|
118
|
-
if (q) this.updateClient(client, q);
|
|
119
|
-
};
|
|
120
|
-
clients.set(client, handler);
|
|
121
|
-
|
|
122
|
-
// register request handler, if defined
|
|
123
|
-
client.request?.addEventListener('value', handler);
|
|
124
|
-
|
|
125
|
-
// TODO analyze / consolidate queries?
|
|
126
|
-
handler();
|
|
128
|
+
client.requestQuery();
|
|
127
129
|
}
|
|
128
130
|
|
|
129
131
|
disconnect(client) {
|
|
130
132
|
const { clients, filterGroups } = this;
|
|
131
133
|
if (!clients.has(client)) return;
|
|
132
|
-
const handler = clients.get(client);
|
|
133
134
|
clients.delete(client);
|
|
134
135
|
filterGroups.get(client.filterBy)?.remove(client);
|
|
135
|
-
client.request?.removeEventListener(handler);
|
|
136
136
|
}
|
|
137
137
|
}
|
package/src/DataTileIndexer.js
CHANGED
|
@@ -49,13 +49,14 @@ export class DataTileIndexer {
|
|
|
49
49
|
const activeView = this.activeView = getActiveView(active);
|
|
50
50
|
if (!activeView) return false; // active selection clause not compatible
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
this.mc.logger().warn('DATA TILE INDEX CONSTRUCTION');
|
|
53
53
|
|
|
54
54
|
// create a selection with the active client removed
|
|
55
55
|
const sel = this.selection.clone().update({ source });
|
|
56
56
|
|
|
57
57
|
// generate data tile indices
|
|
58
58
|
const indices = this.indices = new Map;
|
|
59
|
+
const promises = [];
|
|
59
60
|
for (const client of clients) {
|
|
60
61
|
if (sel.cross && skipClient(client, active)) continue;
|
|
61
62
|
const index = getIndexColumns(client);
|
|
@@ -76,10 +77,10 @@ export class DataTileIndexer {
|
|
|
76
77
|
const id = (fnv_hash(sql) >>> 0).toString(16);
|
|
77
78
|
const table = `tile_index_${id}`;
|
|
78
79
|
indices.set(client, { table, ...index });
|
|
79
|
-
createIndex(this.mc, table, sql);
|
|
80
|
+
promises.push(createIndex(this.mc, table, sql));
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
return
|
|
83
|
+
return promises;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
async update() {
|
|
@@ -185,16 +186,17 @@ async function createIndex(mc, table, query) {
|
|
|
185
186
|
try {
|
|
186
187
|
await mc.exec(`CREATE TEMP TABLE IF NOT EXISTS ${table} AS ${query}`);
|
|
187
188
|
} catch (err) {
|
|
188
|
-
|
|
189
|
+
mc.logger().error(err);
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
const NO_INDEX = { from: NaN };
|
|
194
|
+
|
|
192
195
|
function getIndexColumns(client) {
|
|
196
|
+
if (!client.filterIndexable) return NO_INDEX;
|
|
193
197
|
const q = client.query();
|
|
194
198
|
const from = getBaseTable(q);
|
|
195
|
-
if (!from || !q.groupby
|
|
196
|
-
return { from: NaN }; // early exit
|
|
197
|
-
}
|
|
199
|
+
if (!from || !q.groupby) return NO_INDEX;
|
|
198
200
|
const g = new Set(q.groupby().map(c => c.column));
|
|
199
201
|
|
|
200
202
|
let aggr = [];
|
package/src/FilterGroup.js
CHANGED
|
@@ -2,11 +2,11 @@ import { DataTileIndexer } from './DataTileIndexer.js';
|
|
|
2
2
|
import { throttle } from './util/throttle.js';
|
|
3
3
|
|
|
4
4
|
export class FilterGroup {
|
|
5
|
-
constructor(
|
|
6
|
-
this.mc =
|
|
5
|
+
constructor(coordinator, selection, index = true) {
|
|
6
|
+
this.mc = coordinator;
|
|
7
7
|
this.selection = selection;
|
|
8
8
|
this.clients = new Set();
|
|
9
|
-
this.indexer = index ? new DataTileIndexer(mc, selection) : null;
|
|
9
|
+
this.indexer = index ? new DataTileIndexer(this.mc, selection) : null;
|
|
10
10
|
|
|
11
11
|
const { value, activate } = this.handlers = {
|
|
12
12
|
value: throttle(() => this.update()),
|
package/src/MosaicClient.js
CHANGED
|
@@ -1,41 +1,105 @@
|
|
|
1
|
+
import { coordinator } from './Coordinator.js';
|
|
2
|
+
import { throttle } from './util/throttle.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Base class for Mosaic clients.
|
|
6
|
+
*/
|
|
1
7
|
export class MosaicClient {
|
|
8
|
+
/**
|
|
9
|
+
* Constructor.
|
|
10
|
+
* @param {*} filterSelection An optional selection to interactively filter
|
|
11
|
+
* this client's data. If provided, a coordinator will re-query and update
|
|
12
|
+
* the client when the selection updates.
|
|
13
|
+
*/
|
|
2
14
|
constructor(filterSelection) {
|
|
3
15
|
this._filterBy = filterSelection;
|
|
16
|
+
this._requestUpdate = throttle(() => this.requestQuery(), true);
|
|
4
17
|
}
|
|
5
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Return this client's filter selection.
|
|
21
|
+
*/
|
|
6
22
|
get filterBy() {
|
|
7
23
|
return this._filterBy;
|
|
8
24
|
}
|
|
9
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Return a boolean indicating if the client query can be indexed. Should
|
|
28
|
+
* return true if changes to the filterBy selection does not change the
|
|
29
|
+
* groupby domain of the client query.
|
|
30
|
+
*/
|
|
10
31
|
get filterIndexable() {
|
|
11
32
|
return true;
|
|
12
33
|
}
|
|
13
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Return an array of fields queried by this client.
|
|
37
|
+
*/
|
|
14
38
|
fields() {
|
|
15
39
|
return null;
|
|
16
40
|
}
|
|
17
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Called by the coordinator to set the field statistics for this client.
|
|
44
|
+
* @returns {this}
|
|
45
|
+
*/
|
|
18
46
|
fieldStats() {
|
|
19
47
|
return this;
|
|
20
48
|
}
|
|
21
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Return a query specifying the data needed by this client.
|
|
52
|
+
*/
|
|
22
53
|
query() {
|
|
23
54
|
return null;
|
|
24
55
|
}
|
|
25
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Called by the coordinator to inform the client that a query is pending.
|
|
59
|
+
*/
|
|
26
60
|
queryPending() {
|
|
27
61
|
return this;
|
|
28
62
|
}
|
|
29
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Called by the coordinator to return a query result.
|
|
66
|
+
*/
|
|
30
67
|
queryResult() {
|
|
31
68
|
return this;
|
|
32
69
|
}
|
|
33
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Called by the coordinator to report a query execution error.
|
|
73
|
+
*/
|
|
34
74
|
queryError(error) {
|
|
35
75
|
console.error(error);
|
|
36
76
|
return this;
|
|
37
77
|
}
|
|
38
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Request the coordinator to execute a query for this client.
|
|
81
|
+
* If an explicit query is not provided, the client query method will
|
|
82
|
+
* be called, filtered by the current filterBy selection.
|
|
83
|
+
*/
|
|
84
|
+
requestQuery(query) {
|
|
85
|
+
const q = query || this.query(this.filterBy?.predicate(this));
|
|
86
|
+
return coordinator().requestQuery(this, q);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Request that the coordinator perform a throttled update of this client
|
|
91
|
+
* using the default query. Unlike requestQuery, for which every call will
|
|
92
|
+
* result in an executed query, multiple calls to requestUpdate may be
|
|
93
|
+
* consolidated into a single update.
|
|
94
|
+
*/
|
|
95
|
+
requestUpdate() {
|
|
96
|
+
this._requestUpdate();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Requests a client update.
|
|
101
|
+
* For example to (re-)render an interface component.
|
|
102
|
+
*/
|
|
39
103
|
update() {
|
|
40
104
|
return this;
|
|
41
105
|
}
|
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { distinct } from './util/distinct.js';
|
|
2
|
+
|
|
3
|
+
export function isParam(x) {
|
|
4
|
+
return x instanceof Param;
|
|
3
5
|
}
|
|
4
6
|
|
|
5
|
-
export class
|
|
7
|
+
export class Param {
|
|
6
8
|
constructor(value) {
|
|
7
9
|
this._value = value;
|
|
8
10
|
this._listeners = new Map;
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
static value(value) {
|
|
14
|
+
return new Param(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
get value() {
|
|
12
18
|
return this._value;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
update(value, { force } = {}) {
|
|
16
|
-
const changed = this._value
|
|
22
|
+
const changed = distinct(this._value, value);
|
|
17
23
|
if (changed) this._value = value;
|
|
18
24
|
if (changed || force) this.emit('value', this.value);
|
|
19
25
|
return this;
|
|
@@ -21,7 +27,7 @@ export class Signal {
|
|
|
21
27
|
|
|
22
28
|
addEventListener(type, callback) {
|
|
23
29
|
let list = this._listeners.get(type) || [];
|
|
24
|
-
if (list.
|
|
30
|
+
if (!list.includes(callback)) {
|
|
25
31
|
list = list.concat(callback);
|
|
26
32
|
}
|
|
27
33
|
this._listeners.set(type, list);
|
package/src/QueryCache.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
const requestIdle = typeof requestIdleCallback !== 'undefined'
|
|
2
|
+
? requestIdleCallback
|
|
3
|
+
: setTimeout;
|
|
4
|
+
|
|
5
|
+
export const voidCache = () => ({
|
|
6
|
+
get: () => undefined,
|
|
7
|
+
set: (key, result) => result,
|
|
8
|
+
clear: () => {}
|
|
9
|
+
});
|
|
10
|
+
|
|
1
11
|
export class QueryCache {
|
|
2
12
|
constructor({
|
|
3
13
|
max = 1000, // max entries
|
|
@@ -22,19 +32,9 @@ export class QueryCache {
|
|
|
22
32
|
|
|
23
33
|
set(key, promise) {
|
|
24
34
|
const { cache, max } = this;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
console.log(`Query: ${Math.round(performance.now() - now)}`);
|
|
29
|
-
return result;
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
cache.set(key, { last: now, promise });
|
|
33
|
-
if (cache.size > max) {
|
|
34
|
-
setTimeout(() => this.evict());
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return receive;
|
|
35
|
+
cache.set(key, { last: performance.now(), promise });
|
|
36
|
+
if (cache.size > max) requestIdle(() => this.evict());
|
|
37
|
+
return promise;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
evict() {
|
package/src/Selection.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { or } from '@uwdata/mosaic-sql';
|
|
2
|
-
import {
|
|
2
|
+
import { Param } from './Param.js';
|
|
3
3
|
import { skipClient } from './util/skip-client.js';
|
|
4
4
|
|
|
5
5
|
export function isSelection(x) {
|
|
6
6
|
return x instanceof Selection;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
export class Selection extends
|
|
9
|
+
export class Selection extends Param {
|
|
10
10
|
|
|
11
11
|
static intersect() {
|
|
12
12
|
return new Selection();
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { MosaicClient } from './MosaicClient.js';
|
|
2
2
|
export { Coordinator, coordinator } from './Coordinator.js';
|
|
3
3
|
export { Selection, isSelection } from './Selection.js';
|
|
4
|
-
export {
|
|
4
|
+
export { Param, isParam } from './Param.js';
|
|
5
|
+
export { distinct } from './util/distinct.js';
|
|
5
6
|
export { sqlFrom } from './util/sql-from.js';
|
|
6
7
|
export { throttle } from './util/throttle.js';
|
|
7
8
|
export { restClient } from './clients/rest.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function distinct(a, b) {
|
|
2
|
+
return a === b ? false
|
|
3
|
+
: a instanceof Date && b instanceof Date ? +a !== +b
|
|
4
|
+
: Array.isArray(a) && Array.isArray(b) ? distinctArray(a, b)
|
|
5
|
+
: true;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function distinctArray(a, b) {
|
|
9
|
+
if (a.length !== b.length) return true;
|
|
10
|
+
for (let i = 0; i < a.length; ++i) {
|
|
11
|
+
if (a[i] !== b[i]) return true;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Query, count, isNull, max, min } from '@uwdata/mosaic-sql';
|
|
2
|
+
|
|
3
|
+
export const Count = 'count';
|
|
4
|
+
export const Nulls = 'nulls';
|
|
5
|
+
export const Max = 'max';
|
|
6
|
+
export const Min = 'min';
|
|
7
|
+
export const Distinct = 'distinct';
|
|
8
|
+
|
|
9
|
+
export const Stats = { Count, Nulls, Max, Min, Distinct };
|
|
10
|
+
|
|
11
|
+
export const statMap = {
|
|
12
|
+
[Count]: count,
|
|
13
|
+
[Distinct]: column => count(column).distinct(),
|
|
14
|
+
[Max]: max,
|
|
15
|
+
[Min]: min,
|
|
16
|
+
[Nulls]: column => count().where(isNull(column))
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function summarize({ table, column }, stats) {
|
|
20
|
+
return Query
|
|
21
|
+
.from(table)
|
|
22
|
+
.select(stats.map(s => [s, statMap[s](column)]));
|
|
23
|
+
}
|
package/src/util/throttle.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
const NIL = {};
|
|
2
|
+
|
|
3
|
+
export function throttle(callback, debounce = false) {
|
|
2
4
|
let curr;
|
|
3
5
|
let next;
|
|
6
|
+
let pending = NIL;
|
|
4
7
|
|
|
5
8
|
function invoke(event) {
|
|
6
9
|
curr = callback(event).then(() => {
|
|
7
10
|
if (next) {
|
|
8
|
-
const value = next;
|
|
11
|
+
const { value } = next;
|
|
9
12
|
next = null;
|
|
10
13
|
invoke(value);
|
|
11
14
|
} else {
|
|
@@ -15,8 +18,23 @@ export function throttle(callback) {
|
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
function enqueue(event) {
|
|
18
|
-
next = event;
|
|
21
|
+
next = { event };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function process(event) {
|
|
25
|
+
curr ? enqueue(event) : invoke(event);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function delay(event) {
|
|
29
|
+
if (pending !== event) {
|
|
30
|
+
requestAnimationFrame(() => {
|
|
31
|
+
const e = pending;
|
|
32
|
+
pending = NIL;
|
|
33
|
+
process(e);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
pending = event;
|
|
19
37
|
}
|
|
20
38
|
|
|
21
|
-
return
|
|
39
|
+
return debounce ? delay : process;
|
|
22
40
|
}
|