From 06f460b3644fea75ed972e78b918b9a17153d7c3 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Wed, 21 Oct 2020 14:17:07 -0400 Subject: [PATCH 1/2] Don't dist `perspective` to `perspective-viewer` or other packages --- docs/md/js.md | 133 +++--------------- examples/blocks/package.json | 2 +- examples/blocks/src/csv/csv.js | 19 +-- examples/blocks/src/custom/index.html | 3 +- .../src/config/plugin.config.js | 4 +- .../src/config/webpack.config.js | 2 - .../src/ts/psp_widget.ts | 16 +-- .../perspective-jupyterlab/src/ts/view.ts | 39 +++-- .../test/html/resize.html | 7 +- .../test/js/unit/view.spec.js | 16 ++- .../test/html/shares-template.html | 5 +- .../test/html/simple-template.html | 5 +- .../test/html/themed-template.html | 5 +- .../test/html/superstore.html | 4 +- packages/perspective-viewer/index.d.ts | 9 +- packages/perspective-viewer/src/js/row.js | 2 +- packages/perspective-viewer/src/js/viewer.js | 78 ++-------- .../src/js/viewer/perspective_element.js | 18 +-- .../perspective-viewer/test/html/blank.html | 8 +- .../test/html/superstore.html | 8 +- .../perspective-viewer/test/js/leaks.spec.js | 46 ++---- .../test/js/mutation_tests.js | 29 ---- .../test/js/regressions.spec.js | 18 ++- .../perspective-viewer/test/js/slow.spec.js | 22 ++- packages/perspective/index.d.ts | 2 + .../perspective/src/config/common.config.js | 3 +- .../perspective/src/js/perspective.node.js | 1 + .../perspective/src/js/view_formatters.js | 3 +- packages/perspective/test/js/leaks.spec.js | 34 +++++ 29 files changed, 205 insertions(+), 336 deletions(-) delete mode 100644 packages/perspective-viewer/test/js/mutation_tests.js create mode 100644 packages/perspective/test/js/leaks.spec.js diff --git a/docs/md/js.md b/docs/md/js.md index 5811ee0a87..39ce81b8b8 100644 --- a/docs/md/js.md +++ b/docs/md/js.md @@ -20,88 +20,6 @@ For understanding Perspective's core concepts and vocabulary, see the For example code, see the [examples directory](https://github.com/finos/perspective/tree/master/examples) on GitHub. -## Quick Start - -First, make sure that Perspective [is installed](/docs/md/installation.html), -and that you have the library accessible through your Javascript bundler. - -If Perspective was added using a script tag, see the -[From CDN](/docs/md/installation.html#from-cdn) section of the installation -notes for a quick example. - -### Importing `perspective-viewer` - -To integrate Perspective with an existing data source in your application, -you'll need to import the following modules: - -```javascript -import "@finos/perspective-viewer"; -import "@finos/perspective-viewer-datagrid"; -import "@finos/perspective-viewer-d3fc"; -``` - -`perspective-viewer` provides a widget for users to transform and analyze -their data, while `perspective-viewer-datagrid` and `perspective-viewer-d3fc` -provide fast, flexible grid and chart visualization plugins, respectively. - -These modules register the `` -[Web Component](https://www.webcomponents.org/introduction) for use within -your application's HTML: - -```html - -``` - -### Loading data - -`` offers an attribute API that can be accessed through a -reference to the HTML element: - -```javascript -const viewer = document.getElementsByTagName("perspective-viewer")[0]; -``` - -Use the viewer's `load()` method to provide it with data in JSON, CSV, or -Apache Arrow format: - -```javascript -const data = getData(); // returns a Javascript object -viewer.load(data); // loads the data in `perspective-viewer` -``` - -### Updating data - -When your dataset updates with new information, call the viewer's `update()` -method in order to update `perspective-viewer`. There is no need to refresh or -re-create the viewer, or to accumulate your data elsewhere, as Perspective will -handle everything for you: - -```javascript -// Assume that new ticks are delivered via websocket -websocket.onmessage = function(event) { - viewer.update(event.data); -}; -``` - -### Configuring `perspective-viewer` - -`` defaults to showing a grid view of the entire dataset -without any transformations applied. To configure your viewer to show a -different visualization on load or transform the dataset, use the viewer's attribute API: - -```html - - -``` - -For more details about the full attribute API, see the -[``](#setting--reading-viewer-configuration-via-attributes)section of this user guide. - ## Module Structure Perspective is designed for flexibility, allowing developers to pick and choose @@ -156,19 +74,19 @@ Depending on your requirements, you may need just one, or all Perspective modules. Some basic guidelines to help you decide what is most appropriate for your project: -- For Perspective as a simple, browser-based data visualization widget, import: - - - `@finos/perspective-viewer`, detailed [here](#perspective-viewer-web-component) - - `@finos/perspective-viewer-datagrid` for data grids - - `@finos/perspective-viewer-d3fc` for charting - - The core data engine `@finos/perspective` is a dependency of these packages - and does not need to be imported on its own for basic usage. - - For Perspective's high-performance streaming data engine (in WebAssembly), or for a purely Node.js based application, import: - `@finos/perspective`, as detailed [here](#perspective-library) +- For Perspective as a simple, browser-based data visualization widget, you + will need to import: + + - `@finos/perspective`, as detailed [here](#perspective-library) + - `@finos/perspective-viewer`, detailed [here](#perspective-viewer-web-component) + - `@finos/perspective-viewer-datagrid` for data grids + - `@finos/perspective-viewer-d3fc` for charting + - For more complex cases, such as [sharing tables between viewers](#sharing-a-table-between-multiple-perspective-viewers) and @@ -653,12 +571,20 @@ These can be directly linked in your HTML: ### Loading data into `` -Data can be loaded into `` using the `load()` method in -Javascript with JSON, CSV, or an `ArrayBuffer` in the Apache Arrow format. +Data can be loaded into `` in the form of a `Table()` via +the `load()` method. -If `perspective-viewer` is imported via an inline ` - + + + @@ -30,7 +31,7 @@ window.__CSV__ = xhr.response; window.__WIDGET__ = new PerspectiveLumino.PerspectiveWidget(); container.appendChild(window.__WIDGET__.node); - window.__WIDGET__.load(xhr.response); + window.__WIDGET__.load(perspective.worker().table(xhr.response)); window.__WIDGET__.plugin = "datagrid"; } xhr.send(null); diff --git a/packages/perspective-jupyterlab/test/js/unit/view.spec.js b/packages/perspective-jupyterlab/test/js/unit/view.spec.js index 4e0306edaf..a7524a846e 100644 --- a/packages/perspective-jupyterlab/test/js/unit/view.spec.js +++ b/packages/perspective-jupyterlab/test/js/unit/view.spec.js @@ -6,6 +6,7 @@ * the Apache License 2.0. The full license can be found in the LICENSE file. * */ + import {MockManager} from "../mocks/manager"; import {PerspectiveJupyterClient} from "../../../src/ts/client"; import {PerspectiveJupyterWidget} from "../../../src/ts/widget"; @@ -342,7 +343,10 @@ describe("PerspectiveView", function() { const widget_mock = PerspectiveJupyterWidget.mock.instances[0]; const load_args = widget_mock.load.mock.calls[0][0]; - expect(load_args).toEqual(data); + const result = await load_args.view().to_columns(); + result.b = result.b.map(x => new Date(x)); + + expect(result).toEqual(data); }); it("Should correctly load a dataset with options", async function() { @@ -370,8 +374,14 @@ describe("PerspectiveView", function() { const widget_mock = PerspectiveJupyterWidget.mock.instances[0]; const load_args = widget_mock.load.mock.calls[0]; - expect(load_args[0]).toEqual(data); - expect(load_args[1]).toEqual(options); + const result = await load_args[0].view().to_columns(); + result.b = result.b.map(x => new Date(x)); + + expect(result).toEqual(data); + + // TODO there is no way to verify this in perspective API + // currently ... + // expect(load_args[1]).toEqual(options); }); it("Should correctly update a dataset", async function() { diff --git a/packages/perspective-viewer-d3fc/test/html/shares-template.html b/packages/perspective-viewer-d3fc/test/html/shares-template.html index 7415b1e3a1..0f19815057 100644 --- a/packages/perspective-viewer-d3fc/test/html/shares-template.html +++ b/packages/perspective-viewer-d3fc/test/html/shares-template.html @@ -12,7 +12,7 @@ - + @@ -32,7 +32,8 @@ var xhr = new XMLHttpRequest(); xhr.open('GET', 'shares.csv', true); xhr.onload = function () { - document.getElementsByTagName('perspective-viewer')[0].load(xhr.response); + const table = perspective.worker().table(xhr.response); + document.getElementsByTagName('perspective-viewer')[0].load(table); } xhr.send(null); diff --git a/packages/perspective-viewer-d3fc/test/html/simple-template.html b/packages/perspective-viewer-d3fc/test/html/simple-template.html index a3a9418db4..59f8be9657 100644 --- a/packages/perspective-viewer-d3fc/test/html/simple-template.html +++ b/packages/perspective-viewer-d3fc/test/html/simple-template.html @@ -11,7 +11,7 @@ - + @@ -31,7 +31,8 @@ var xhr = new XMLHttpRequest(); xhr.open('GET', 'superstore.csv', true); xhr.onload = function () { - document.getElementsByTagName('perspective-viewer')[0].load(xhr.response); + const table = perspective.worker().table(xhr.response); + document.getElementsByTagName('perspective-viewer')[0].load(table); } xhr.send(null); diff --git a/packages/perspective-viewer-d3fc/test/html/themed-template.html b/packages/perspective-viewer-d3fc/test/html/themed-template.html index b96fa0a404..ceee261c93 100644 --- a/packages/perspective-viewer-d3fc/test/html/themed-template.html +++ b/packages/perspective-viewer-d3fc/test/html/themed-template.html @@ -11,7 +11,7 @@ - + @@ -59,7 +59,8 @@ var xhr = new XMLHttpRequest(); xhr.open('GET', 'superstore.csv', true); xhr.onload = function () { - document.getElementsByTagName('perspective-viewer')[0].load(xhr.response); + const table = perspective.worker().table(xhr.response); + document.getElementsByTagName('perspective-viewer')[0].load(table); } xhr.send(null); diff --git a/packages/perspective-viewer-datagrid/test/html/superstore.html b/packages/perspective-viewer-datagrid/test/html/superstore.html index 102aee5510..9d2fe62204 100644 --- a/packages/perspective-viewer-datagrid/test/html/superstore.html +++ b/packages/perspective-viewer-datagrid/test/html/superstore.html @@ -11,6 +11,7 @@ + @@ -39,7 +40,8 @@ var xhr = new XMLHttpRequest(); xhr.open('GET', 'superstore.csv', true); xhr.onload = function () { - document.getElementsByTagName('perspective-viewer')[0].load(xhr.response); + const table = perspective.worker().table(xhr.response); + document.getElementsByTagName('perspective-viewer')[0].load(table); } xhr.send(null); diff --git a/packages/perspective-viewer/index.d.ts b/packages/perspective-viewer/index.d.ts index a83bc0ff6b..5acf7c875e 100644 --- a/packages/perspective-viewer/index.d.ts +++ b/packages/perspective-viewer/index.d.ts @@ -8,16 +8,12 @@ */ import React from "react"; -import {Table, TableData, TableOptions, Schema} from "@finos/perspective"; +import {Table, View} from "@finos/perspective"; export interface HTMLPerspectiveViewerElement extends PerspectiveViewerOptions, HTMLElement { - load(data: TableData | Table | View, options?: TableOptions): void; - load(schema: Schema, options?: TableOptions): void; - update(data: TableData): void; + load(data: Table): void; notifyResize(): void; delete(delete_table: boolean): Promise; - clear(): void; - replace(data: TableData): void; flush(): Promise; getEditPort(): Promise; toggleConfig(): void; @@ -26,6 +22,7 @@ export interface HTMLPerspectiveViewerElement extends PerspectiveViewerOptions, restore(x: any): Promise; restyleElement(): void; readonly table?: Table; + readonly view?: View; } interface ComputedColumn { column: string; diff --git a/packages/perspective-viewer/src/js/row.js b/packages/perspective-viewer/src/js/row.js index 021dc55e3c..daee8e7933 100644 --- a/packages/perspective-viewer/src/js/row.js +++ b/packages/perspective-viewer/src/js/row.js @@ -14,7 +14,7 @@ import awesomplete_style from "!!css-loader!awesomplete/awesomplete.css"; import {bindTemplate} from "./utils.js"; -import perspective from "@finos/perspective"; +import * as perspective from "@finos/perspective/dist/esm/config/constants.js"; import {get_type_config} from "@finos/perspective/dist/esm/config"; import template from "../html/row.html"; diff --git a/packages/perspective-viewer/src/js/viewer.js b/packages/perspective-viewer/src/js/viewer.js index 8c0dae36c1..44a56930ad 100755 --- a/packages/perspective-viewer/src/js/viewer.js +++ b/packages/perspective-viewer/src/js/viewer.js @@ -528,22 +528,6 @@ class PerspectiveViewer extends ActionElement { this.dispatchEvent(new Event("perspective-config-update")); } - /** - * This element's `perspective` worker instance. This property is not - * reflected as an HTML attribute, and is readonly; it can be effectively - * set however by calling the `load() method with a `perspective.table` - * instance from the preferred worker. - * - * @readonly - * @example - * let elem = document.getElementById('my_viewer'); - * let table = elem.worker.table([{x:1, y:2}]); - * elem.load(table); - */ - get worker() { - return this._get_worker(); - } - /** * This element's `perspective.table` instance. * @@ -569,16 +553,13 @@ class PerspectiveViewer extends ActionElement { * element, its internal `perspective.table` will also be deleted. * * @async - * @param {any} data The data to load. Works with the same input types - * supported by `perspective.table`. + * @param {any} data The data to load, as a `perspective.Table` or + * `Promise`. * @returns {Promise} A promise which resolves once the data is loaded * and a `perspective.view` has been created. * @fires module:perspective_viewer~PerspectiveViewer#perspective-click * PerspectiveViewer#perspective-view-update * ]); - * @example Load CSV - * const my_viewer = document.getElementById('#my_viewer'); - * my_viewer.load("x,y\n1,a\n2,b"); * @example Load perspective.table * const my_viewer = document.getElementById('#my_viewer'); * const tbl = perspective.table("x,y\n1,a\n2,b"); @@ -588,7 +569,7 @@ class PerspectiveViewer extends ActionElement { * const tbl = async () => perspective.table("x,y\n1,a\n2,b"); * my_viewer.load(tbl); */ - async load(data, options) { + async load(data) { let table; if (data instanceof Promise) { table = await data; @@ -599,8 +580,7 @@ class PerspectiveViewer extends ActionElement { if (data.type === "table") { table = data; } else { - table = this.worker.table(data, options); - table._owner_viewer = this; + throw new Error(`Unrecognized input type ${typeof data}. Please use a \`perspective.Table()\``); } } if (this.isConnected) { @@ -610,27 +590,6 @@ class PerspectiveViewer extends ActionElement { } } - /** - * Updates this element's `perspective.table` with new data. - * - * @param {any} data The data to load. Works with the same input types - * supported by `perspective.table.update`. - * @fires PerspectiveViewer#perspective-view-update - * @example - * const my_viewer = document.getElementById('#my_viewer'); - * my_viewer.update([ - * {x: 1, y: 'a'}, - * {x: 2, y: 'b'} - * ]); - */ - update(data) { - if (this._table === undefined) { - this.load(data); - } else { - this._table.update(data); - } - } - /** * Determine whether to reflow the viewer and redraw. * @@ -660,21 +619,20 @@ class PerspectiveViewer extends ActionElement { } /** - * Deletes this element's data and clears it's internal state (but not its - * user state). This (or the underlying `perspective.table`'s equivalent - * method) must be called in order for its memory to be reclaimed. + * Deletes this element and clears it's internal state (but not its + * user state). This (or the underlying `perspective.view`'s equivalent + * method) must be called in order for its memory to be reclaimed, as well + * as the recipcorcal method on the `perspective.table` which this viewer is + * bound to. * - * @param {Boolean} delete_table Should a delete call also be made to the - * underlying `table()`. * @returns {Promise} Whether or not this call resulted in the * underlying `perspective.table` actually being deleted. */ - delete(delete_table = true) { - let x = this._clear_state(delete_table); + delete() { + let x = this._clear_state(); if (this._plugin.delete) { this._plugin.delete.call(this); } - window.removeEventListener("load", this._resize_handler); window.removeEventListener("resize", this._resize_handler); return x; } @@ -763,20 +721,6 @@ class PerspectiveViewer extends ActionElement { } } - /** - * Clears the rows in the current {@link table}. - */ - clear() { - this._table?.clear(); - } - - /** - * Replaces all rows in the current {@link table}. - */ - replace(data) { - this._table ? this._table.replace(data) : this._load(data); - } - /** * Reset's this element's view state and attributes to default. Does not * delete this element's `perspective.table` or otherwise modify the data diff --git a/packages/perspective-viewer/src/js/viewer/perspective_element.js b/packages/perspective-viewer/src/js/viewer/perspective_element.js index e3e2f0ef4e..82dd00e136 100644 --- a/packages/perspective-viewer/src/js/viewer/perspective_element.js +++ b/packages/perspective-viewer/src/js/viewer/perspective_element.js @@ -11,7 +11,7 @@ import debounce from "lodash/debounce"; import isEqual from "lodash/isEqual"; import {html, render} from "lit-html"; -import perspective from "@finos/perspective"; +import * as perspective from "@finos/perspective/dist/esm/config/constants.js"; import {get_type_config} from "@finos/perspective/dist/esm/config"; import {CancelTask} from "./cancel_task.js"; @@ -559,7 +559,7 @@ export class PerspectiveElement extends StateElement { } } - _clear_state(clear_table = true) { + _clear_state() { if (this._task) { this._task.cancel(); } @@ -571,13 +571,6 @@ export class PerspectiveElement extends StateElement { view.remove_update(this._view_updater); view.remove_delete(); } - if (this._table && clear_table) { - const table = this._table; - this._table = undefined; - if (table._owner_viewer && table._owner_viewer === this) { - all.push(table.delete()); - } - } return Promise.all(all); } @@ -603,11 +596,4 @@ export class PerspectiveElement extends StateElement { } }; } - - _get_worker() { - if (this._table) { - return this._table._worker; - } - return perspective.shared_worker(); - } } diff --git a/packages/perspective-viewer/test/html/blank.html b/packages/perspective-viewer/test/html/blank.html index d59bbbe437..82e0f941b4 100644 --- a/packages/perspective-viewer/test/html/blank.html +++ b/packages/perspective-viewer/test/html/blank.html @@ -10,9 +10,9 @@ - - - + + + @@ -25,7 +25,7 @@ diff --git a/packages/perspective-viewer/test/html/superstore.html b/packages/perspective-viewer/test/html/superstore.html index 73a4d8c913..d30948eebe 100644 --- a/packages/perspective-viewer/test/html/superstore.html +++ b/packages/perspective-viewer/test/html/superstore.html @@ -11,8 +11,8 @@ - - + + @@ -29,7 +29,9 @@ xhr.open('GET', 'superstore.csv', true); xhr.onload = function () { window.__CSV__ = xhr.response; - document.getElementsByTagName('perspective-viewer')[0].load(xhr.response); + window.__WORKER__ = perspective.worker(); + const table = window.__WORKER__.table(xhr.response); + document.getElementsByTagName('perspective-viewer')[0].load(table); } xhr.send(null); diff --git a/packages/perspective-viewer/test/js/leaks.spec.js b/packages/perspective-viewer/test/js/leaks.spec.js index f720aea4b0..5df1e42b8c 100644 --- a/packages/perspective-viewer/test/js/leaks.spec.js +++ b/packages/perspective-viewer/test/js/leaks.spec.js @@ -14,42 +14,20 @@ utils.with_server({}, () => { describe.page( "superstore.html", () => { - // must specify timeout AND viewport - test.capture( - "doesn't leak tables.", - async page => { - const viewer = await page.$("perspective-viewer"); - await page.shadow_click("perspective-viewer", "#config_button"); - for (var i = 0; i < 100; i++) { - await page.evaluate(element => element.load(window.__CSV__), viewer); - await page.waitForSelector("perspective-viewer:not([updating])"); - } - await page.evaluate( - element => - element.load( - window.__CSV__ - .split("\n") - .slice(0, 10) - .join("\n") - ), - viewer - ); - await page.waitForSelector("perspective-viewer:not([updating])"); - }, - {timeout: 60000} - ); - test.capture( "doesn't leak elements.", async page => { let viewer = await page.$("perspective-viewer"); - //await page.shadow_click("perspective-viewer", "#config_button"); + await page.evaluate(viewer => { + window.__TABLE__ = viewer.table; + }, viewer); + for (var i = 0; i < 100; i++) { viewer = await page.$("perspective-viewer"); - await page.evaluate(element => { + await page.evaluate(async element => { element.delete(); - document.innerHTML = ""; - document.getElementsByTagName("perspective-viewer")[0].load(window.__CSV__); + document.innerHTML = ""; + await document.getElementsByTagName("perspective-viewer")[0].load(window.__TABLE__); }, viewer); await page.waitForSelector("perspective-viewer:not([updating])"); } @@ -57,10 +35,12 @@ utils.with_server({}, () => { await page.evaluate( element => element.load( - window.__CSV__ - .split("\n") - .slice(0, 10) - .join("\n") + window.__WORKER__.table( + window.__CSV__ + .split("\n") + .slice(0, 10) + .join("\n") + ) ), viewer ); diff --git a/packages/perspective-viewer/test/js/mutation_tests.js b/packages/perspective-viewer/test/js/mutation_tests.js deleted file mode 100644 index 6d4cff1211..0000000000 --- a/packages/perspective-viewer/test/js/mutation_tests.js +++ /dev/null @@ -1,29 +0,0 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -exports.default = function() { - test.capture("replaces all rows.", async page => { - const viewer = await page.$("perspective-viewer"); - await page.shadow_click("perspective-viewer", "#config_button"); - const json = await page.evaluate(async element => { - let json = await element.view.to_json(); - return json.slice(10, 20); - }, viewer); - expect(json.length).toEqual(10); - await page.evaluate( - async (element, json) => { - element.replace(json); - }, - viewer, - json - ); - // FIXME This is due to update() not triggering flush() semantics - await page.waitFor(1000); - }); -}; diff --git a/packages/perspective-viewer/test/js/regressions.spec.js b/packages/perspective-viewer/test/js/regressions.spec.js index 98e8466f43..c6d9a3048f 100644 --- a/packages/perspective-viewer/test/js/regressions.spec.js +++ b/packages/perspective-viewer/test/js/regressions.spec.js @@ -38,8 +38,9 @@ utils.with_server({}, () => { const viewer = await page.$("perspective-viewer"); await page.evaluate( (viewer, data, schema) => { - viewer.load(schema); - viewer.update(data); + const table = window.WORKER.table(schema); + viewer.load(table); + table.update(data); }, viewer, getData(2, 1), @@ -50,8 +51,9 @@ utils.with_server({}, () => { await page.evaluate( (viewer, data, schema) => { - viewer.load(schema); - viewer.update(data); + const table = window.WORKER.table(schema); + viewer.load(table); + table.update(data); }, viewer, getData(3, 2), @@ -70,7 +72,8 @@ utils.with_server({}, () => { const byte_length = await page.evaluate( async (viewer, data) => { const arrow = Uint8Array.from([...data].map(ch => ch.charCodeAt())).buffer; - viewer.load(arrow); + const table = window.WORKER.table(arrow); + viewer.load(table); // force _process to run - otherwise reading // bytelength will return the un-transfered arrow. await viewer.table.size(); @@ -97,9 +100,10 @@ utils.with_server({}, () => { const arrow = arrows.int_float_str_arrow.slice(); const byte_length = await page.evaluate( async (viewer, data, schema) => { - viewer.load(schema); + const table = window.WORKER.table(schema); + viewer.load(table); const arrow = Uint8Array.from([...data].map(ch => ch.charCodeAt())).buffer; - viewer.update(arrow); + table.update(arrow); // force _process to run - otherwise reading // bytelength will return the un-transfered arrow. await viewer.table.size(); diff --git a/packages/perspective-viewer/test/js/slow.spec.js b/packages/perspective-viewer/test/js/slow.spec.js index bc04184890..534365731d 100644 --- a/packages/perspective-viewer/test/js/slow.spec.js +++ b/packages/perspective-viewer/test/js/slow.spec.js @@ -9,14 +9,30 @@ const utils = require("@finos/perspective-test"); -const mutation_tests = require("./mutation_tests.js"); const path = require("path"); utils.with_server({}, () => { describe.page( "superstore.html", - () => { - mutation_tests.default(); + function() { + test.capture("replaces all rows.", async page => { + const viewer = await page.$("perspective-viewer"); + await page.shadow_click("perspective-viewer", "#config_button"); + const json = await page.evaluate(async element => { + let json = await element.view.to_json(); + return json.slice(10, 20); + }, viewer); + expect(json.length).toEqual(10); + await page.evaluate( + async (element, json) => { + element.table.replace(json); + }, + viewer, + json + ); + // FIXME This is due to update() not triggering flush() semantics + await page.waitFor(1000); + }); }, {root: path.join(__dirname, "..", "..")} ); diff --git a/packages/perspective/index.d.ts b/packages/perspective/index.d.ts index fab7363409..4697e16d46 100644 --- a/packages/perspective/index.d.ts +++ b/packages/perspective/index.d.ts @@ -126,6 +126,8 @@ declare module "@finos/perspective" { export type Table = { columns(): Array; + clear(): Promise; + replace(data: TableData): Promise; delete(): Promise; on_delete(callback: Function): void; computed_schema(): Promise; diff --git a/packages/perspective/src/config/common.config.js b/packages/perspective/src/config/common.config.js index cfa3cfbceb..631fcffe3f 100644 --- a/packages/perspective/src/config/common.config.js +++ b/packages/perspective/src/config/common.config.js @@ -1,8 +1,7 @@ -const webpack = require("webpack"); const path = require("path"); const PerspectivePlugin = require("@finos/perspective-webpack-plugin"); const TerserPlugin = require("terser-webpack-plugin"); -const plugins = [new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /(en|es|fr)$/)]; +const plugins = []; function common({build_worker, no_minify, inline} = {}) { plugins.push(new PerspectivePlugin({build_worker: build_worker, workerLoaderOptions: {inline, name: "[name].worker.js"}, wasmLoaderOptions: {inline, name: "[name]"}})); diff --git a/packages/perspective/src/js/perspective.node.js b/packages/perspective/src/js/perspective.node.js index b4c434f7a4..1f04250a0c 100644 --- a/packages/perspective/src/js/perspective.node.js +++ b/packages/perspective/src/js/perspective.node.js @@ -186,6 +186,7 @@ const websocket = url => { return new WebSocketClient(new WebSocket(url)); }; +module.exports.worker = () => module.exports; module.exports.websocket = websocket; module.exports.perspective_assets = perspective_assets; module.exports.WebSocketServer = WebSocketServer; diff --git a/packages/perspective/src/js/view_formatters.js b/packages/perspective/src/js/view_formatters.js index 1cc2e6a72f..10254d42a1 100644 --- a/packages/perspective/src/js/view_formatters.js +++ b/packages/perspective/src/js/view_formatters.js @@ -34,7 +34,8 @@ const csvFormatter = Object.assign({}, jsonFormatter, { case "object": case "string": // CSV escapes with double double quotes, for real. - // [Section 2.7 of the fake CSV spec](https://tools.ietf.org/html/rfc4180) + // Section 2.7 of the fake + // [CSV spec](https://tools.ietf.org/html/rfc4180) return x.indexOf(delimiter) > -1 ? `"${x.replace(/\"/g, '""')}"` : x.toString(); case "number": return x; diff --git a/packages/perspective/test/js/leaks.spec.js b/packages/perspective/test/js/leaks.spec.js new file mode 100644 index 0000000000..8685d85f30 --- /dev/null +++ b/packages/perspective/test/js/leaks.spec.js @@ -0,0 +1,34 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +const perspective = require("../../dist/cjs/perspective.node.js"); +const fs = require("fs"); +const path = require("path"); + +const arr = fs.readFileSync(path.join(__dirname, "../../../../node_modules/superstore-arrow/superstore.arrow")).buffer; + +describe("perspective.table", function() { + it("does not leak memory after delete() is called on a table", async () => { + for (var i = 0; i < 500; i++) { + const table = perspective.table(arr.slice()); + expect(await table.size()).toEqual(9994); + table.delete(); + } + }); + + it("does not leak memory after delete() is called on a view", async () => { + const table = perspective.table(arr.slice()); + for (var i = 0; i < 500; i++) { + const view = table.view(); + expect(await view.num_rows()).toEqual(9994); + view.delete(); + } + table.delete(); + }); +}); From 5ca506191f6b4337064db8fc6e928bc884780554 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Mon, 2 Nov 2020 13:50:51 -0500 Subject: [PATCH 2/2] Code review fixes --- packages/perspective-jupyterlab/src/ts/view.ts | 4 ++++ .../perspective-jupyterlab/test/js/unit/view.spec.js | 11 ++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/perspective-jupyterlab/src/ts/view.ts b/packages/perspective-jupyterlab/src/ts/view.ts index 108c8ee22d..0bd89920a6 100644 --- a/packages/perspective-jupyterlab/src/ts/view.ts +++ b/packages/perspective-jupyterlab/src/ts/view.ts @@ -327,6 +327,10 @@ export class PerspectiveView extends DOMWidgetView { kernel_view.to_arrow().then((arrow: ArrayBuffer) => { const table = this.client_worker.table(arrow, table_options); this.pWidget.load(table); + kernel_view.on_update( + updated => table.update(updated.delta), + {mode: "row"} + ); }); } } else { diff --git a/packages/perspective-jupyterlab/test/js/unit/view.spec.js b/packages/perspective-jupyterlab/test/js/unit/view.spec.js index a7524a846e..8007105f5d 100644 --- a/packages/perspective-jupyterlab/test/js/unit/view.spec.js +++ b/packages/perspective-jupyterlab/test/js/unit/view.spec.js @@ -101,7 +101,7 @@ describe("PerspectiveView", function() { view = await manager.create_view(model)(); const mock_client = PerspectiveJupyterClient.mock.instances[0]; mock_client.open_table.mockReturnValue({ - view: jest.fn() + view: jest.fn().mockImplementation(name => ({to_arrow: jest.fn().mockImplementation(() => new Promise(() => null)), name})) }); // Mock the output of open_table() so `view()` is valid @@ -130,7 +130,7 @@ describe("PerspectiveView", function() { view = await manager.create_view(model)(); const mock_client = PerspectiveJupyterClient.mock.instances[0]; mock_client.open_table.mockReturnValue({ - view: jest.fn() + view: jest.fn().mockImplementation(name => ({to_arrow: jest.fn().mockImplementation(() => new Promise(() => null)), name})) }); const table_name = uuid(); @@ -160,7 +160,7 @@ describe("PerspectiveView", function() { view = await manager.create_view(model)(); const mock_client = PerspectiveJupyterClient.mock.instances[0]; mock_client.open_table.mockReturnValue({ - view: jest.fn() + view: jest.fn().mockImplementation(name => ({to_arrow: jest.fn().mockImplementation(() => new Promise(() => null)), name})) }); const table_name = uuid(); @@ -378,10 +378,7 @@ describe("PerspectiveView", function() { result.b = result.b.map(x => new Date(x)); expect(result).toEqual(data); - - // TODO there is no way to verify this in perspective API - // currently ... - // expect(load_args[1]).toEqual(options); + expect(await load_args[0].get_index()).toEqual("a"); }); it("Should correctly update a dataset", async function() {