@uwdata/mosaic-core 0.9.0 → 0.11.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.
@@ -1,26 +1,32 @@
1
1
  import { Query, agg, sql } from '@uwdata/mosaic-sql';
2
2
  import { MosaicClient } from '../MosaicClient.js';
3
3
 
4
- export const NO_INDEX = { from: NaN };
5
-
6
4
  /**
7
5
  * Determine data cube index columns for a given Mosaic client.
8
6
  * @param {MosaicClient} client The Mosaic client.
9
7
  * @returns An object with necessary column data to generate data
10
- * cube index columns, null if an invalid or unsupported expression
11
- * is encountered, or NO_INDEX if the client is not indexable.
8
+ * cube index columns, or null if the client is not indexable or
9
+ * the client query contains an invalid or unsupported expression.
12
10
  */
13
11
  export function indexColumns(client) {
14
- if (!client.filterIndexable) return NO_INDEX;
12
+ if (!client.filterIndexable) return null;
15
13
  const q = client.query();
16
- const from = getBaseTable(q);
17
- if (typeof from !== 'string' || !q.groupby) return NO_INDEX;
18
- const g = new Set(q.groupby().map(c => c.column));
14
+ const from = getBase(q, q => q.from()?.[0].from.table);
15
+
16
+ // bail if no base table or the query is not analyzable
17
+ if (typeof from !== 'string' || !q.select) return null;
19
18
 
20
19
  const aggr = []; // list of output aggregate columns
21
20
  const dims = []; // list of grouping dimension columns
22
21
  const aux = {}; // auxiliary columns needed by aggregates
23
22
 
23
+ const avg = ref => {
24
+ const name = ref.column;
25
+ // @ts-ignore
26
+ const expr = getBase(q, q => q.select().find(c => c.as === name)?.expr);
27
+ return `(SELECT AVG(${expr ?? ref}) FROM "${from}")`;
28
+ };
29
+
24
30
  for (const entry of q.select()) {
25
31
  const { as, expr: { aggregate, args } } = entry;
26
32
  const op = aggregate?.toUpperCase?.();
@@ -47,32 +53,32 @@ export function indexColumns(client) {
47
53
  case 'VARIANCE':
48
54
  case 'VAR_SAMP':
49
55
  aux[as] = null;
50
- aggr.push({ [as]: varianceExpr(aux, args[0], from) });
56
+ aggr.push({ [as]: varianceExpr(aux, args[0], avg) });
51
57
  break;
52
58
  case 'VAR_POP':
53
59
  aux[as] = null;
54
- aggr.push({ [as]: varianceExpr(aux, args[0], from, false) });
60
+ aggr.push({ [as]: varianceExpr(aux, args[0], avg, false) });
55
61
  break;
56
62
  case 'STDDEV':
57
63
  case 'STDDEV_SAMP':
58
64
  aux[as] = null;
59
- aggr.push({ [as]: agg`SQRT(${varianceExpr(aux, args[0], from)})` });
65
+ aggr.push({ [as]: agg`SQRT(${varianceExpr(aux, args[0], avg)})` });
60
66
  break;
61
67
  case 'STDDEV_POP':
62
68
  aux[as] = null;
63
- aggr.push({ [as]: agg`SQRT(${varianceExpr(aux, args[0], from, false)})` });
69
+ aggr.push({ [as]: agg`SQRT(${varianceExpr(aux, args[0], avg, false)})` });
64
70
  break;
65
71
  case 'COVAR_SAMP':
66
72
  aux[as] = null;
67
- aggr.push({ [as]: covarianceExpr(aux, args, from) });
73
+ aggr.push({ [as]: covarianceExpr(aux, args, avg) });
68
74
  break;
69
75
  case 'COVAR_POP':
70
76
  aux[as] = null;
71
- aggr.push({ [as]: covarianceExpr(aux, args, from, false) });
77
+ aggr.push({ [as]: covarianceExpr(aux, args, avg, false) });
72
78
  break;
73
79
  case 'CORR':
74
80
  aux[as] = null;
75
- aggr.push({ [as]: corrExpr(aux, args, from) });
81
+ aggr.push({ [as]: corrExpr(aux, args, avg) });
76
82
  break;
77
83
 
78
84
  // regression statistics
@@ -90,27 +96,27 @@ export function indexColumns(client) {
90
96
  break;
91
97
  case 'REGR_SYY':
92
98
  aux[as] = null;
93
- aggr.push({ [as]: regrVarExpr(aux, 0, args, from) });
99
+ aggr.push({ [as]: regrVarExpr(aux, 0, args, avg) });
94
100
  break;
95
101
  case 'REGR_SXX':
96
102
  aux[as] = null;
97
- aggr.push({ [as]: regrVarExpr(aux, 1, args, from) });
103
+ aggr.push({ [as]: regrVarExpr(aux, 1, args, avg) });
98
104
  break;
99
105
  case 'REGR_SXY':
100
106
  aux[as] = null;
101
- aggr.push({ [as]: covarianceExpr(aux, args, from, null) });
107
+ aggr.push({ [as]: covarianceExpr(aux, args, avg, null) });
102
108
  break;
103
109
  case 'REGR_SLOPE':
104
110
  aux[as] = null;
105
- aggr.push({ [as]: regrSlopeExpr(aux, args, from) });
111
+ aggr.push({ [as]: regrSlopeExpr(aux, args, avg) });
106
112
  break;
107
113
  case 'REGR_INTERCEPT':
108
114
  aux[as] = null;
109
- aggr.push({ [as]: regrInterceptExpr(aux, args, from) });
115
+ aggr.push({ [as]: regrInterceptExpr(aux, args, avg) });
110
116
  break;
111
117
  case 'REGR_R2':
112
118
  aux[as] = null;
113
- aggr.push({ [as]: agg`(${corrExpr(aux, args, from)}) ** 2` });
119
+ aggr.push({ [as]: agg`(${corrExpr(aux, args, avg)}) ** 2` });
114
120
  break;
115
121
 
116
122
  // aggregates that commute directly
@@ -127,11 +133,14 @@ export function indexColumns(client) {
127
133
 
128
134
  // otherwise, check if dimension
129
135
  default:
130
- if (g.has(as)) dims.push(as);
136
+ if (!aggregate) dims.push(as);
131
137
  else return null; // unsupported aggregate
132
138
  }
133
139
  }
134
140
 
141
+ // bail if the query has no aggregates
142
+ if (!aggr.length) return null;
143
+
135
144
  return { from, dims, aggr, aux };
136
145
  }
137
146
 
@@ -161,29 +170,30 @@ function sanitize(col) {
161
170
  }
162
171
 
163
172
  /**
164
- * Identify a single base (source) table of a query.
173
+ * Identify a shared base (source) query and extract a value from it.
174
+ * This method is used to find a shared base table name or extract
175
+ * the original column name within a base table.
165
176
  * @param {Query} query The input query.
166
- * @returns {string | undefined | NaN} the base table name, or
177
+ * @param {(q: Query) => any} get A getter function to extract
178
+ * a value from a base query.
179
+ * @returns {string | undefined | NaN} the base query value, or
167
180
  * `undefined` if there is no source table, or `NaN` if the
168
181
  * query operates over multiple source tables.
169
182
  */
170
- function getBaseTable(query) {
183
+ function getBase(query, get) {
171
184
  const subq = query.subqueries;
172
185
 
173
186
  // select query
174
- if (query.select) {
175
- const from = query.from();
176
- // @ts-ignore
177
- if (!from.length) return undefined;
178
- if (subq.length === 0) return from[0].from.table;
187
+ if (query.select && subq.length === 0) {
188
+ return get(query);
179
189
  }
180
190
 
181
191
  // handle set operations / subqueries
182
- const base = getBaseTable(subq[0]);
192
+ const base = getBase(subq[0], get);
183
193
  for (let i = 1; i < subq.length; ++i) {
184
- const from = getBaseTable(subq[i]);
185
- if (from === undefined) continue;
186
- if (from !== base) return NaN;
194
+ const value = getBase(subq[i], get);
195
+ if (value === undefined) continue;
196
+ if (value !== base) return NaN;
187
197
  }
188
198
  return base;
189
199
  }
@@ -222,17 +232,6 @@ function avgExpr(aux, as, arg) {
222
232
  return agg`(SUM("${as}" * ${n.name}) / ${n})`;
223
233
  }
224
234
 
225
- /**
226
- * Generate a scalar subquery for a global average.
227
- * This value can be used to mean-center data.
228
- * @param {*} x Souce data table column.
229
- * @param {string} from The source data table name.
230
- * @returns A scalar aggregate query
231
- */
232
- function avg(x, from) {
233
- return sql`(SELECT AVG(${x}) FROM "${from}")`;
234
- }
235
-
236
235
  /**
237
236
  * Generate an expression for calculating argmax over data partitions.
238
237
  * As a side effect, this method adds a column to the input *aux* object
@@ -281,18 +280,18 @@ function argminExpr(aux, as, [, y]) {
281
280
  * sufficient statistics) to include in the data cube aggregation.
282
281
  * @param {*} x The source data table column. This may be a string,
283
282
  * column reference, SQL expression, or other string-coercible value.
284
- * @param {string} from The source data table name.
283
+ * @param {(field: any) => string} avg Global average query generator.
285
284
  * @param {boolean} [correction=true] A flag for whether a Bessel
286
285
  * correction should be applied to compute the sample variance
287
286
  * rather than the populatation variance.
288
287
  * @returns An aggregate expression for calculating variance over
289
288
  * pre-aggregated data partitions.
290
289
  */
291
- function varianceExpr(aux, x, from, correction = true) {
290
+ function varianceExpr(aux, x, avg, correction = true) {
292
291
  const n = countExpr(aux, x);
293
292
  const ssq = auxName('rssq', x); // residual sum of squares
294
293
  const sum = auxName('rsum', x); // residual sum
295
- const delta = sql`${x} - ${avg(x, from)}`;
294
+ const delta = sql`${x} - ${avg(x)}`;
296
295
  aux[ssq] = agg`SUM((${delta}) ** 2)`;
297
296
  aux[sum] = agg`SUM(${delta})`;
298
297
  const adj = correction ? ` - 1` : ''; // Bessel correction
@@ -310,7 +309,7 @@ function varianceExpr(aux, x, from, correction = true) {
310
309
  * sufficient statistics) to include in the data cube aggregation.
311
310
  * @param {any[]} args Source data table columns. The entries may be strings,
312
311
  * column references, SQL expressions, or other string-coercible values.
313
- * @param {string} from The source data table name.
312
+ * @param {(field: any) => string} avg Global average query generator.
314
313
  * @param {boolean|null} [correction=true] A flag for whether a Bessel
315
314
  * correction should be applied to compute the sample covariance rather
316
315
  * than the populatation covariance. If null, an expression for the
@@ -318,11 +317,11 @@ function varianceExpr(aux, x, from, correction = true) {
318
317
  * @returns An aggregate expression for calculating covariance over
319
318
  * pre-aggregated data partitions.
320
319
  */
321
- function covarianceExpr(aux, args, from, correction = true) {
320
+ function covarianceExpr(aux, args, avg, correction = true) {
322
321
  const n = regrCountExpr(aux, args);
323
- const sxy = regrSumXYExpr(aux, args, from);
324
- const sx = regrSumExpr(aux, 1, args, from);
325
- const sy = regrSumExpr(aux, 0, args, from);
322
+ const sxy = regrSumXYExpr(aux, args, avg);
323
+ const sx = regrSumExpr(aux, 1, args, avg);
324
+ const sy = regrSumExpr(aux, 0, args, avg);
326
325
  const adj = correction === null ? '' // do not divide by count
327
326
  : correction ? ` / (${n} - 1)` // Bessel correction (sample)
328
327
  : ` / ${n}`; // no correction (population)
@@ -341,17 +340,17 @@ function covarianceExpr(aux, args, from, correction = true) {
341
340
  * sufficient statistics) to include in the data cube aggregation.
342
341
  * @param {any[]} args Source data table columns. The entries may be strings,
343
342
  * column references, SQL expressions, or other string-coercible values.
344
- * @param {string} from The source data table name.
343
+ * @param {(field: any) => string} avg Global average query generator.
345
344
  * @returns An aggregate expression for calculating correlation over
346
345
  * pre-aggregated data partitions.
347
346
  */
348
- function corrExpr(aux, args, from) {
347
+ function corrExpr(aux, args, avg) {
349
348
  const n = regrCountExpr(aux, args);
350
- const sxy = regrSumXYExpr(aux, args, from);
351
- const sxx = regrSumSqExpr(aux, 1, args, from);
352
- const syy = regrSumSqExpr(aux, 0, args, from);
353
- const sx = regrSumExpr(aux, 1, args, from);
354
- const sy = regrSumExpr(aux, 0, args, from);
349
+ const sxy = regrSumXYExpr(aux, args, avg);
350
+ const sxx = regrSumSqExpr(aux, 1, args, avg);
351
+ const syy = regrSumSqExpr(aux, 0, args, avg);
352
+ const sx = regrSumExpr(aux, 1, args, avg);
353
+ const sy = regrSumExpr(aux, 0, args, avg);
355
354
  const vx = agg`(${sxx} - (${sx} ** 2) / ${n})`;
356
355
  const vy = agg`(${syy} - (${sy} ** 2) / ${n})`;
357
356
  return agg`(${sxy} - ${sx} * ${sy} / ${n}) / SQRT(${vx} * ${vy})`;
@@ -385,14 +384,14 @@ function regrCountExpr(aux, [y, x]) {
385
384
  * @param {number} i An index indicating which argument column to sum.
386
385
  * @param {any[]} args Source data table columns. The entries may be strings,
387
386
  * column references, SQL expressions, or other string-coercible values.
388
- * @param {string} from The source data table name.
387
+ * @param {(field: any) => string} avg Global average query generator.
389
388
  * @returns An aggregate expression over pre-aggregated data partitions.
390
389
  */
391
- function regrSumExpr(aux, i, args, from) {
390
+ function regrSumExpr(aux, i, args, avg) {
392
391
  const v = args[i];
393
392
  const o = args[1 - i];
394
393
  const sum = auxName('rs', v);
395
- aux[sum] = agg`SUM(${v} - ${avg(v, from)}) FILTER (${o} IS NOT NULL)`;
394
+ aux[sum] = agg`SUM(${v} - ${avg(v)}) FILTER (${o} IS NOT NULL)`;
396
395
  return agg`SUM(${sum})`
397
396
  }
398
397
 
@@ -407,14 +406,14 @@ function regrSumExpr(aux, i, args, from) {
407
406
  * @param {number} i An index indicating which argument column to sum.
408
407
  * @param {any[]} args Source data table columns. The entries may be strings,
409
408
  * column references, SQL expressions, or other string-coercible values.
410
- * @param {string} from The source data table name.
409
+ * @param {(field: any) => string} avg Global average query generator.
411
410
  * @returns An aggregate expression over pre-aggregated data partitions.
412
411
  */
413
- function regrSumSqExpr(aux, i, args, from) {
412
+ function regrSumSqExpr(aux, i, args, avg) {
414
413
  const v = args[i];
415
414
  const u = args[1 - i];
416
415
  const ssq = auxName('rss', v);
417
- aux[ssq] = agg`SUM((${v} - ${avg(v, from)}) ** 2) FILTER (${u} IS NOT NULL)`;
416
+ aux[ssq] = agg`SUM((${v} - ${avg(v)}) ** 2) FILTER (${u} IS NOT NULL)`;
418
417
  return agg`SUM(${ssq})`
419
418
  }
420
419
 
@@ -428,13 +427,13 @@ function regrSumSqExpr(aux, i, args, from) {
428
427
  * sufficient statistics) to include in the data cube aggregation.
429
428
  * @param {any[]} args Source data table columns. The entries may be strings,
430
429
  * column references, SQL expressions, or other string-coercible values.
431
- * @param {string} from The source data table name.
430
+ * @param {(field: any) => string} avg Global average query generator.
432
431
  * @returns An aggregate expression over pre-aggregated data partitions.
433
432
  */
434
- function regrSumXYExpr(aux, args, from) {
433
+ function regrSumXYExpr(aux, args, avg) {
435
434
  const [y, x] = args;
436
435
  const sxy = auxName('sxy', y, x);
437
- aux[sxy] = agg`SUM((${x} - ${avg(x, from)}) * (${y} - ${avg(y, from)}))`;
436
+ aux[sxy] = agg`SUM((${x} - ${avg(x)}) * (${y} - ${avg(y)}))`;
438
437
  return agg`SUM(${sxy})`;
439
438
  }
440
439
 
@@ -487,14 +486,14 @@ function regrAvgYExpr(aux, args) {
487
486
  * @param {number} i The index of the argument to compute the variance for.
488
487
  * @param {any[]} args Source data table columns. The entries may be strings,
489
488
  * column references, SQL expressions, or other string-coercible values.
490
- * @param {string} from The source data table name.
489
+ * @param {(field: any) => string} avg Global average query generator.
491
490
  * @returns An aggregate expression for calculating variance over
492
491
  * pre-aggregated data partitions.
493
492
  */
494
- function regrVarExpr(aux, i, args, from) {
493
+ function regrVarExpr(aux, i, args, avg) {
495
494
  const n = regrCountExpr(aux, args);
496
- const sum = regrSumExpr(aux, i, args, from);
497
- const ssq = regrSumSqExpr(aux, i, args, from);
495
+ const sum = regrSumExpr(aux, i, args, avg);
496
+ const ssq = regrSumSqExpr(aux, i, args, avg);
498
497
  return agg`(${ssq} - (${sum} ** 2 / ${n}))`;
499
498
  }
500
499
 
@@ -507,13 +506,13 @@ function regrVarExpr(aux, i, args, from) {
507
506
  * sufficient statistics) to include in the data cube aggregation.
508
507
  * @param {any[]} args Source data table columns. The entries may be strings,
509
508
  * column references, SQL expressions, or other string-coercible values.
510
- * @param {string} from The source data table name.
509
+ * @param {(field: any) => string} avg Global average query generator.
511
510
  * @returns An aggregate expression for calculating regression slopes over
512
511
  * pre-aggregated data partitions.
513
512
  */
514
- function regrSlopeExpr(aux, args, from) {
515
- const cov = covarianceExpr(aux, args, from, null);
516
- const varx = regrVarExpr(aux, 1, args, from);
513
+ function regrSlopeExpr(aux, args, avg) {
514
+ const cov = covarianceExpr(aux, args, avg, null);
515
+ const varx = regrVarExpr(aux, 1, args, avg);
517
516
  return agg`(${cov}) / ${varx}`;
518
517
  }
519
518
 
@@ -526,13 +525,13 @@ function regrSlopeExpr(aux, args, from) {
526
525
  * sufficient statistics) to include in the data cube aggregation.
527
526
  * @param {any[]} args Source data table columns. The entries may be strings,
528
527
  * column references, SQL expressions, or other string-coercible values.
529
- * @param {string} from The source data table name.
528
+ * @param {(field: any) => string} avg Global average query generator.
530
529
  * @returns An aggregate expression for calculating regression intercepts over
531
530
  * pre-aggregated data partitions.
532
531
  */
533
- function regrInterceptExpr(aux, args, from) {
532
+ function regrInterceptExpr(aux, args, avg) {
534
533
  const ax = regrAvgXExpr(aux, args);
535
534
  const ay = regrAvgYExpr(aux, args);
536
- const m = regrSlopeExpr(aux, args, from);
535
+ const m = regrSlopeExpr(aux, args, avg);
537
536
  return agg`${ay} - (${m}) * ${ax}`;
538
537
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Test if a value is a Flechette Arrow table.
3
+ * We use a "duck typing" approach and check for a getChild function.
4
+ * @param {*} values The value to test
5
+ * @returns {values is import('@uwdata/flechette').Table}
6
+ * true if the value duck types as Arrow data
7
+ */
8
+ export function isArrowTable(values) {
9
+ return typeof values?.getChild === 'function';
10
+ }
@@ -1,85 +1,84 @@
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(
1
+ export class PriorityQueue {
2
+ /**
3
+ * Create a new priority queue instance.
4
+ * @param {number} ranks An integer number of rank-order priority levels.
5
+ */
6
+ constructor(ranks) {
7
+ // one list for each integer priority level
8
+ this.queue = Array.from(
9
9
  { length: ranks },
10
10
  () => ({ head: null, tail: null })
11
11
  );
12
+ }
12
13
 
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
- },
14
+ /**
15
+ * Indicate if the queue is empty.
16
+ * @returns {boolean} true if empty, false otherwise.
17
+ */
18
+ isEmpty() {
19
+ return this.queue.every(list => !list.head);
20
+ }
21
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
- }
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 = this.queue[rank];
31
+ if (!list) {
32
+ throw new Error(`Invalid queue priority rank: ${rank}`);
33
+ }
34
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
- },
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
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
- },
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 this.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
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
- };
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 this.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
+ }
85
84
  }
@@ -1,9 +1,42 @@
1
- export function queryResult() {
2
- let resolve;
3
- let reject;
4
- const p = new Promise((r, e) => { resolve = r; reject = e; });
5
- return Object.assign(p, {
6
- fulfill: value => (resolve(value), p),
7
- reject: err => (reject(err), p)
8
- });
1
+ /**
2
+ * A query result Promise that can allows external callers
3
+ * to resolve or reject the Promise.
4
+ */
5
+ export class QueryResult extends Promise {
6
+ /**
7
+ * Create a new query result Promise.
8
+ */
9
+ constructor() {
10
+ let resolve;
11
+ let reject;
12
+ super((r, e) => {
13
+ resolve = r;
14
+ reject = e;
15
+ });
16
+ this._resolve = resolve;
17
+ this._reject = reject;
18
+ }
19
+
20
+ /**
21
+ * Resolve the result Promise with the provided value.
22
+ * @param {*} value The result value.
23
+ * @returns {this}
24
+ */
25
+ fulfill(value) {
26
+ this._resolve(value);
27
+ return this;
28
+ }
29
+
30
+ /**
31
+ * Rejects the result Promise with the provided error.
32
+ * @param {*} error The error value.
33
+ * @returns {this}
34
+ */
35
+ reject(error) {
36
+ this._reject(error);
37
+ return this;
38
+ }
9
39
  }
40
+
41
+ // necessary to make Promise subclass act like a Promise
42
+ QueryResult.prototype.constructor = Promise;
@@ -1,12 +1,22 @@
1
1
  const NIL = {};
2
2
 
3
+ /**
4
+ * Throttle invocations of a callback function. The callback must return
5
+ * a Promise. Upon repeated invocation, the callback will not be invoked
6
+ * until a prior Promise resolves. If multiple invocations occurs while
7
+ * waiting, only the most recent invocation will be pending.
8
+ * @param {(event: *) => Promise} callback The callback function.
9
+ * @param {boolean} [debounce=true] Flag indicating if invocations
10
+ * should also be debounced within the current animation frame.
11
+ * @returns A new function that throttles access to the callback.
12
+ */
3
13
  export function throttle(callback, debounce = false) {
4
14
  let curr;
5
15
  let next;
6
16
  let pending = NIL;
7
17
 
8
18
  function invoke(event) {
9
- curr = callback(event).then(() => {
19
+ curr = callback(event).finally(() => {
10
20
  if (next) {
11
21
  const { value } = next;
12
22
  next = null;