@uwdata/mosaic-core 0.0.1 → 0.2.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.
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Event dispatcher supporting asynchronous updates.
3
+ * If an event handler callback returns a Promise, this dispatcher will
4
+ * wait for all such Promises to settle before dispatching future events
5
+ * of the same type.
6
+ */
7
+ export class AsyncDispatch {
8
+
9
+ /**
10
+ * Create a new asynchronous dispatcher instance.
11
+ */
12
+ constructor() {
13
+ this._callbacks = new Map;
14
+ }
15
+
16
+ /**
17
+ * Add an event listener callback for the provided event type.
18
+ * @param {string} type The event type.
19
+ * @param {(value: *) => Promise?} callback The event handler
20
+ * callback function to add. If the callback has already been
21
+ * added for the event type, this method has no effect.
22
+ */
23
+ addEventListener(type, callback) {
24
+ if (!this._callbacks.has(type)) {
25
+ this._callbacks.set(type, {
26
+ callbacks: new Set,
27
+ pending: null,
28
+ queue: new DispatchQueue()
29
+ });
30
+ }
31
+ const entry = this._callbacks.get(type);
32
+ entry.callbacks.add(callback);
33
+ }
34
+
35
+ /**
36
+ * Remove an event listener callback for the provided event type.
37
+ * @param {string} type The event type.
38
+ * @param {(value: *) => Promise?} callback The event handler
39
+ * callback function to remove.
40
+ */
41
+ removeEventListener(type, callback) {
42
+ const entry = this._callbacks.get(type);
43
+ if (entry) {
44
+ entry.callbacks.delete(callback);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Lifecycle method that returns the event value to emit.
50
+ * This default implementation simply returns the input value as-is.
51
+ * Subclasses may override this method to implement custom transformations
52
+ * prior to emitting an event value to all listeners.
53
+ * @param {string} type The event type.
54
+ * @param {*} value The event value.
55
+ * @returns The (possibly transformed) event value to emit.
56
+ */
57
+ willEmit(type, value) {
58
+ return value;
59
+ }
60
+
61
+ /**
62
+ * Lifecycle method that returns a filter function for updating the
63
+ * queue of unemitted event values prior to enqueueing a new value.
64
+ * This default implementation simply returns null, indicating that
65
+ * any other unemitted event values should be dropped (that is, all
66
+ * queued events are filtered)
67
+ * @param {*} value The new event value that will be enqueued.
68
+ * @returns {(value: *) => boolean|null} A dispatch queue filter
69
+ * function, or null if all unemitted event values should be filtered.
70
+ */
71
+ emitQueueFilter() {
72
+ // removes all pending items
73
+ return null;
74
+ }
75
+
76
+ /**
77
+ * Cancel all unemitted event values for the given event type.
78
+ * @param {string} type The event type.
79
+ */
80
+ cancel(type) {
81
+ const entry = this._callbacks.get(type);
82
+ entry?.queue.clear();
83
+ }
84
+
85
+ /**
86
+ * Emit an event value to listeners for the given event type.
87
+ * If a previous emit has not yet resolved, the event value
88
+ * will be queued to be emitted later.
89
+ * The actual event value given to listeners will be the result
90
+ * of passing the input value through the emitValue() method.
91
+ * @param {string} type The event type.
92
+ * @param {*} value The event value.
93
+ */
94
+ emit(type, value) {
95
+ const entry = this._callbacks.get(type) || {};
96
+ if (entry.pending) {
97
+ entry.queue.enqueue(value, this.emitQueueFilter(type, value));
98
+ } else {
99
+ const event = this.willEmit(type, value);
100
+ const { callbacks, queue } = entry;
101
+ if (callbacks?.size) {
102
+ const promise = Promise
103
+ .allSettled(Array.from(callbacks, callback => callback(event)))
104
+ .then(() => {
105
+ entry.pending = null;
106
+ if (!queue.isEmpty()) {
107
+ this.emit(type, queue.dequeue());
108
+ }
109
+ });
110
+ entry.pending = promise;
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Queue for managing unemitted event values.
118
+ */
119
+ export class DispatchQueue {
120
+
121
+ /**
122
+ * Create a new dispatch queue instance.
123
+ */
124
+ constructor() {
125
+ this.clear();
126
+ }
127
+
128
+ /**
129
+ * Clear the queue state of all event values.
130
+ */
131
+ clear() {
132
+ this.next = null;
133
+ }
134
+
135
+ /**
136
+ * Indicate if the queue is empty.
137
+ * @returns {boolean} True if queue is empty, false otherwise.
138
+ */
139
+ isEmpty() {
140
+ return !this.next;
141
+ }
142
+
143
+ /**
144
+ * Add a new value to the queue, and optionally filter the
145
+ * current queue content in response.
146
+ * @param {*} value The value to add.
147
+ * @param {(value: *) => boolean} [filter] An optional filter
148
+ * function to apply to existing queue content. If unspecified
149
+ * or falsy, all previously queued values are removed. Otherwise,
150
+ * the provided function is applied to all queue entries. The
151
+ * entry is retained if the filter function returns a truthy value,
152
+ * otherwise the entry is removed.
153
+ */
154
+ enqueue(value, filter) {
155
+ const tail = { value };
156
+ if (filter && this.next) {
157
+ let curr = this;
158
+ while (curr.next) {
159
+ if (filter(curr.next.value)) {
160
+ curr = curr.next;
161
+ } else {
162
+ curr.next = curr.next.next;
163
+ }
164
+ }
165
+ curr.next = tail;
166
+ } else {
167
+ this.next = tail;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Remove and return the next queued event value.
173
+ * @returns {*} The next event value in the queue.
174
+ */
175
+ dequeue() {
176
+ const { next } = this;
177
+ this.next = next?.next;
178
+ return next?.value;
179
+ }
180
+ }
@@ -0,0 +1,58 @@
1
+ const requestIdle = typeof requestIdleCallback !== 'undefined'
2
+ ? requestIdleCallback
3
+ : setTimeout;
4
+
5
+ export const voidCache = () => ({
6
+ get: () => undefined,
7
+ set: (key, value) => value,
8
+ clear: () => {}
9
+ });
10
+
11
+ export function lruCache({
12
+ max = 1000, // max entries
13
+ ttl = 3 * 60 * 60 * 1000 // time-to-live, default 3 hours
14
+ } = {}) {
15
+ let cache = new Map;
16
+
17
+ function evict() {
18
+ const expire = performance.now() - ttl;
19
+ let lruKey = null;
20
+ let lruLast = Infinity;
21
+
22
+ for (const [key, value] of cache) {
23
+ const { last } = value;
24
+
25
+ // least recently used entry seen so far
26
+ if (last < lruLast) {
27
+ lruKey = key;
28
+ lruLast = last;
29
+ }
30
+
31
+ // remove if time since last access exceeds ttl
32
+ if (expire > last) {
33
+ cache.delete(key);
34
+ }
35
+ }
36
+
37
+ // remove lru entry
38
+ if (lruKey) {
39
+ cache.delete(lruKey);
40
+ }
41
+ }
42
+
43
+ return {
44
+ get(key) {
45
+ const entry = cache.get(key);
46
+ if (entry) {
47
+ entry.last = performance.now();
48
+ return entry.value;
49
+ }
50
+ },
51
+ set(key, value) {
52
+ cache.set(key, { last: performance.now(), value });
53
+ if (cache.size > max) requestIdle(evict);
54
+ return value;
55
+ },
56
+ clear() { cache = new Map; }
57
+ };
58
+ }
@@ -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,85 @@
1
+ /**
2
+ * Create a new priority queue instance.
3
+ * @param {number} ranks An integer number of rank-order priority levels.
4
+ * @returns A priority queue instance.
5
+ */
6
+ export function priorityQueue(ranks) {
7
+ // one list for each integer priority level
8
+ const queue = Array.from(
9
+ { length: ranks },
10
+ () => ({ head: null, tail: null })
11
+ );
12
+
13
+ return {
14
+ /**
15
+ * Indicate if the queue is empty.
16
+ * @returns [boolean] true if empty, false otherwise.
17
+ */
18
+ isEmpty() {
19
+ return queue.every(list => !list.head);
20
+ },
21
+
22
+ /**
23
+ * Insert an item into the queue with a given priority rank.
24
+ * @param {*} item The item to add.
25
+ * @param {number} rank The integer priority rank.
26
+ * Priority ranks are integers starting at zero.
27
+ * Lower ranks indicate higher priority.
28
+ */
29
+ insert(item, rank) {
30
+ const list = queue[rank];
31
+ if (!list) {
32
+ throw new Error(`Invalid queue priority rank: ${rank}`);
33
+ }
34
+
35
+ const node = { item, next: null };
36
+ if (list.head === null) {
37
+ list.head = list.tail = node;
38
+ } else {
39
+ list.tail = (list.tail.next = node);
40
+ }
41
+ },
42
+
43
+ /**
44
+ * Remove a set of items from the queue, regardless of priority rank.
45
+ * If a provided item is not in the queue it will be ignored.
46
+ * @param {(item: *) => boolean} test A predicate function to test
47
+ * if an item should be removed (true to drop, false to keep).
48
+ */
49
+ remove(test) {
50
+ for (const list of queue) {
51
+ let { head, tail } = list;
52
+ for (let prev = null, curr = head; curr; prev = curr, curr = curr.next) {
53
+ if (test(curr.item)) {
54
+ if (curr === head) {
55
+ head = curr.next;
56
+ } else {
57
+ prev.next = curr.next;
58
+ }
59
+ if (curr === tail) tail = prev || head;
60
+ }
61
+ }
62
+ list.head = head;
63
+ list.tail = tail;
64
+ }
65
+ },
66
+
67
+ /**
68
+ * Remove and return the next highest priority item.
69
+ * @returns {*} The next item in the queue,
70
+ * or undefined if this queue is empty.
71
+ */
72
+ next() {
73
+ for (const list of queue) {
74
+ const { head } = list;
75
+ if (head !== null) {
76
+ list.head = head.next;
77
+ if (list.tail === head) {
78
+ list.tail = null;
79
+ }
80
+ return head.item;
81
+ }
82
+ }
83
+ }
84
+ };
85
+ }
@@ -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
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Create a new synchronizer instance to aid synchronization
3
+ * of updates on multiple pending operations.
4
+ */
5
+ export function synchronizer() {
6
+ const set = new Set;
7
+ let done;
8
+ let promise = new Promise(resolve => done = resolve);
9
+
10
+ return {
11
+ /**
12
+ * Mark an item as pending.
13
+ * @param {*} item An item to synchronize on.
14
+ */
15
+ pending(item) {
16
+ set.add(item);
17
+ },
18
+ /**
19
+ * Mark a pending item as ready, indicating it is
20
+ * ready for a synchronized update.
21
+ * @param {*} item An item to synchronize on.
22
+ * @returns {boolean} True if the synchronizer is ready to
23
+ * resolve, false otherwise.
24
+ */
25
+ ready(item) {
26
+ set.delete(item);
27
+ return set.size === 0;
28
+ },
29
+ /**
30
+ * Resolve the current synchronization cycle, causing the synchronize
31
+ * promise to resolve and thereby trigger downstream updates.
32
+ */
33
+ resolve() {
34
+ promise = new Promise(resolve => {
35
+ done();
36
+ done = resolve;
37
+ });
38
+ },
39
+ /**
40
+ * The promise for the current synchronization cycle.
41
+ * @return {Promise} The synchronization promise.
42
+ */
43
+ get promise() {
44
+ return promise;
45
+ }
46
+ }
47
+ }
@@ -1,11 +1,14 @@
1
- export function throttle(callback) {
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 event => curr ? enqueue(event) : invoke(event);
39
+ return debounce ? delay : process;
22
40
  }
@@ -0,0 +1,9 @@
1
+ export function voidLogger() {
2
+ return {
3
+ debug() {},
4
+ info() {},
5
+ log() {},
6
+ warn() {},
7
+ error() {}
8
+ };
9
+ }
package/src/QueryCache.js DELETED
@@ -1,65 +0,0 @@
1
- export class QueryCache {
2
- constructor({
3
- max = 1000, // max entries
4
- ttl = 3 * 60 * 60 * 1000 // time-to-live, default 3 hours
5
- } = {}) {
6
- this.max = max;
7
- this.ttl = ttl;
8
- this.clear();
9
- }
10
-
11
- clear() {
12
- this.cache = new Map();
13
- }
14
-
15
- get(key) {
16
- const entry = this.cache.get(key);
17
- if (entry) {
18
- entry.last = performance.now();
19
- return entry.promise;
20
- }
21
- }
22
-
23
- set(key, promise) {
24
- const { cache, max } = this;
25
- const now = performance.now();
26
-
27
- const receive = promise.then(result => {
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;
38
- }
39
-
40
- evict() {
41
- const expire = performance.now() - this.ttl;
42
- let lruKey = null;
43
- let lruLast = Infinity;
44
-
45
- for (const [key, value] of this.cache) {
46
- const { last } = value;
47
-
48
- // least recently used entry seen so far
49
- if (last < lruLast) {
50
- lruKey = key;
51
- lruLast = last;
52
- }
53
-
54
- // remove if time since last access exceeds ttl
55
- if (expire > last) {
56
- this.cache.delete(key);
57
- }
58
- }
59
-
60
- // remove lru entry
61
- if (lruKey) {
62
- this.cache.delete(lruKey);
63
- }
64
- }
65
- }
package/src/Signal.js DELETED
@@ -1,40 +0,0 @@
1
- export function isSignal(x) {
2
- return x instanceof Signal;
3
- }
4
-
5
- export class Signal {
6
- constructor(value) {
7
- this._value = value;
8
- this._listeners = new Map;
9
- }
10
-
11
- get value() {
12
- return this._value;
13
- }
14
-
15
- update(value, { force } = {}) {
16
- const changed = this._value !== value;
17
- if (changed) this._value = value;
18
- if (changed || force) this.emit('value', this.value);
19
- return this;
20
- }
21
-
22
- addEventListener(type, callback) {
23
- let list = this._listeners.get(type) || [];
24
- if (list.indexOf(callback) < 0) {
25
- list = list.concat(callback);
26
- }
27
- this._listeners.set(type, list);
28
- }
29
-
30
- removeEventListener(type, callback) {
31
- const list = this._listeners.get(type);
32
- if (list?.length) {
33
- this._listeners.set(type, list.filter(x => x !== callback));
34
- }
35
- }
36
-
37
- emit(type, event) {
38
- this._listeners.get(type)?.forEach(l => l(event));
39
- }
40
- }
@@ -1,3 +0,0 @@
1
- export function skipClient(client, clause) {
2
- return clause?.clients?.has(client);
3
- }
@@ -1,22 +0,0 @@
1
- import { literalToSQL } from '@uwdata/mosaic-sql';
2
-
3
- export function sqlFrom(data, {
4
- columns = Object.keys(data?.[0] || {})
5
- } = {}) {
6
- let keys = [];
7
- if (Array.isArray(columns)) {
8
- keys = columns;
9
- columns = keys.reduce((m, k) => (m[k] = k, m), {});
10
- } else if (columns) {
11
- keys = Object.keys(columns);
12
- }
13
- if (!keys.length) {
14
- throw new Error('Can not create table from empty column set.');
15
- }
16
- const subq = [];
17
- for (const datum of data) {
18
- const sel = keys.map(k => `${literalToSQL(datum[k])} AS "${columns[k]}"`);
19
- subq.push(`(SELECT ${sel.join(', ')})`);
20
- }
21
- return subq.join(' UNION ALL ');
22
- }