Files
2026-07-13 12:25:07 +08:00

840 lines
32 KiB
JavaScript

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
import { test, expect } from "@perspective-dev/test";
import perspective from "./perspective_client";
import _ from "lodash";
const data = {
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 3, 4],
y: ["a", "b", "c", "d"],
z: [true, false, true, false],
};
const get_random_int = function (min, max) {
// liberally copied from stack
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
function it_old_behavior(name, capture) {
test(name, async function () {
let done;
let result = new Promise((x) => {
done = x;
});
await capture(done);
await result;
});
}
((perspective) => {
test.describe("ports", function () {
test("Should create port IDs in incremental order", async function () {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
table.delete();
});
test("Should create port IDs in incremental order and allow updates on each port", async function () {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [port_id],
y: ["d"],
z: [true],
},
{ port_id },
);
}
const view = await table.view();
const output = await view.to_columns();
expect(await table.size()).toEqual(14);
const expected = {
w: [
1.5, 2.5, 3.5, 4.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5,
1.5, 1.5,
],
x: [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y: [
"a",
"b",
"c",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
],
z: [
true,
false,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
],
};
expect(output).toEqual(expected);
view.delete();
table.delete();
});
test("Should create port IDs in incremental order and allow updates on each port, indexed", async function () {
const table = await perspective.table(data, { index: "w" });
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [1],
y: ["a"],
z: [true],
},
{ port_id },
);
}
expect(await table.size()).toEqual(4);
const view = await table.view();
const output = await view.to_columns();
expect(output).toEqual(data);
view.delete();
table.delete();
});
test("Should create port IDs in incremental order and allow random arbitrary updates on each port, indexed", async function () {
const table = await perspective.table(data, { index: "w" });
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [1],
y: ["a"],
z: [true],
},
{ port_id },
);
}
expect(await table.size()).toEqual(4);
const view = await table.view();
const output = await view.to_columns();
expect(output).toEqual(data);
// arbitarily update from a random port, force append with new pkey
const port = get_random_int(1, 9);
const update_data = {
w: [5.5],
x: [5],
y: ["e"],
z: [true],
};
await table.update(update_data, { port_id: 0 });
const output2 = await view.to_columns();
expect(output2).toEqual({
w: [1.5, 2.5, 3.5, 4.5, 5.5],
x: [1, 2, 3, 4, 5],
y: ["a", "b", "c", "d", "e"],
z: [true, false, true, false, true],
});
// and do it again but this time with null as pkey
let port2 = get_random_int(1, 9);
while (port2 === port) {
port2 = get_random_int(1, 9);
}
const update_data2 = {
w: [null],
x: [6],
y: ["f"],
z: [true],
};
await table.update(update_data2, { port_id: port2 });
const output3 = await view.to_columns();
expect(output3).toEqual({
w: [null, 1.5, 2.5, 3.5, 4.5, 5.5],
x: [6, 1, 2, 3, 4, 5],
y: ["f", "a", "b", "c", "d", "e"],
z: [true, true, false, true, false, true],
});
view.delete();
table.delete();
});
test.describe("View notifications from different ports", function () {
test("All views should be notified by appends on all ports", async function () {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
const view2 = await table.view({
group_by: ["w"],
});
const view3 = await table.view({
group_by: ["w"],
split_by: ["x"],
});
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [port_id],
y: ["d"],
z: [true],
},
{ port_id },
);
}
const output = await view.to_columns();
expect(await table.size()).toEqual(14);
const expected = {
w: [
1.5, 2.5, 3.5, 4.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5, 1.5,
1.5, 1.5, 1.5,
],
x: [1, 2, 3, 4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
y: [
"a",
"b",
"c",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
"d",
],
z: [
true,
false,
true,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
],
};
expect(output).toEqual(expected);
const output2 = await view2.to_columns();
expect(output2).toEqual({
__ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
w: [27, 16.5, 2.5, 3.5, 4.5],
x: [65, 56, 2, 3, 4],
y: [14, 11, 1, 1, 1],
z: [14, 11, 1, 1, 1],
});
const output3 = await view3.to_columns();
expect(output3).toEqual({
__ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
"1|w": [3, 3, null, null, null],
"1|x": [2, 2, null, null, null],
"1|y": [2, 2, null, null, null],
"1|z": [2, 2, null, null, null],
"2|w": [4, 1.5, 2.5, null, null],
"2|x": [4, 2, 2, null, null],
"2|y": [2, 1, 1, null, null],
"2|z": [2, 1, 1, null, null],
"3|w": [5, 1.5, null, 3.5, null],
"3|x": [6, 3, null, 3, null],
"3|y": [2, 1, null, 1, null],
"3|z": [2, 1, null, 1, null],
"4|w": [6, 1.5, null, null, 4.5],
"4|x": [8, 4, null, null, 4],
"4|y": [2, 1, null, null, 1],
"4|z": [2, 1, null, null, 1],
"5|w": [1.5, 1.5, null, null, null],
"5|x": [5, 5, null, null, null],
"5|y": [1, 1, null, null, null],
"5|z": [1, 1, null, null, null],
"6|w": [1.5, 1.5, null, null, null],
"6|x": [6, 6, null, null, null],
"6|y": [1, 1, null, null, null],
"6|z": [1, 1, null, null, null],
"7|w": [1.5, 1.5, null, null, null],
"7|x": [7, 7, null, null, null],
"7|y": [1, 1, null, null, null],
"7|z": [1, 1, null, null, null],
"8|w": [1.5, 1.5, null, null, null],
"8|x": [8, 8, null, null, null],
"8|y": [1, 1, null, null, null],
"8|z": [1, 1, null, null, null],
"9|w": [1.5, 1.5, null, null, null],
"9|x": [9, 9, null, null, null],
"9|y": [1, 1, null, null, null],
"9|z": [1, 1, null, null, null],
"10|w": [1.5, 1.5, null, null, null],
"10|x": [10, 10, null, null, null],
"10|y": [1, 1, null, null, null],
"10|z": [1, 1, null, null, null],
});
view3.delete();
view2.delete();
view.delete();
table.delete();
});
test("All views should be notified by partial updates on all ports", async function () {
const table = await perspective.table(data, { index: "w" });
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
const view2 = await table.view({
group_by: ["w"],
});
const view3 = await table.view({
group_by: ["w"],
split_by: ["x"],
});
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [port_id],
y: ["d"],
z: [false],
},
{ port_id },
);
}
const output = await view.to_columns();
expect(await table.size()).toEqual(4);
const expected = {
w: [1.5, 2.5, 3.5, 4.5],
x: [10, 2, 3, 4],
y: ["d", "b", "c", "d"],
z: [false, false, true, false],
};
expect(output).toEqual(expected);
const output2 = await view2.to_columns();
expect(output2).toEqual({
__ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
w: [12, 1.5, 2.5, 3.5, 4.5],
x: [19, 10, 2, 3, 4],
y: [4, 1, 1, 1, 1],
z: [4, 1, 1, 1, 1],
});
const output3 = await view3.to_columns();
expect(output3).toEqual({
__ROW_PATH__: [[], [1.5], [2.5], [3.5], [4.5]],
"2|w": [2.5, null, 2.5, null, null],
"2|x": [2, null, 2, null, null],
"2|y": [1, null, 1, null, null],
"2|z": [1, null, 1, null, null],
"3|w": [3.5, null, null, 3.5, null],
"3|x": [3, null, null, 3, null],
"3|y": [1, null, null, 1, null],
"3|z": [1, null, null, 1, null],
"4|w": [4.5, null, null, null, 4.5],
"4|x": [4, null, null, null, 4],
"4|y": [1, null, null, null, 1],
"4|z": [1, null, null, null, 1],
"10|w": [1.5, 1.5, null, null, null],
"10|x": [10, 10, null, null, null],
"10|y": [1, 1, null, null, null],
"10|z": [1, 1, null, null, null],
});
view3.delete();
view2.delete();
view.delete();
table.delete();
});
});
test.describe("on_update notifications from different ports", function () {
it_old_behavior(
"All calls to on_update should contain the port ID",
async function (done) {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
// because no updates were called in port 0, start at 1
let last_port_id = 1;
view.on_update(function (updated) {
expect(updated.port_id).toEqual(last_port_id);
if (last_port_id == 10) {
view.delete();
table.delete();
done();
} else {
last_port_id++;
}
});
for (const port_id of port_ids) {
await table.update(
{
w: [1.5],
x: [1],
y: ["a"],
z: [true],
},
{ port_id },
);
}
},
);
it_old_behavior(
"Only the port that was updated should be notified in on_update",
async function (done) {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
const port_id = get_random_int(1, 9);
view.on_update(function (updated) {
expect(updated.port_id).toEqual(port_id);
view.delete();
table.delete();
done();
});
await table.update(
{
w: [1.5],
x: [1],
y: ["a"],
z: [true],
},
{ port_id },
);
},
);
it_old_behavior(
"All ports should be notified in creation order, regardless of what order the update is called.",
async function (done) {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
let last_port_id = 0;
let num_updates = 0;
view.on_update(function (updated) {
expect(updated.port_id).toEqual(last_port_id);
if (num_updates === 10) {
view.delete();
table.delete();
done();
} else {
num_updates++;
last_port_id++;
}
});
const update_order = _.shuffle([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
]);
for (const port_id of update_order) {
table.update(
{
w: [1.5],
x: [1],
y: ["a"],
z: [true],
},
{ port_id },
);
}
},
);
});
test.describe("deltas", function () {
it_old_behavior(
"Deltas should be unique to each port",
async function (done) {
const table = await perspective.table(data);
const port_ids = [];
for (let i = 0; i < 10; i++) {
port_ids.push(await table.make_port());
}
expect(port_ids).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const view = await table.view();
let update_count = 0;
view.on_update(
async function (updated) {
const _t = await perspective.table(updated.delta);
const _v = await _t.view();
const result = await _v.to_columns();
if (updated.port_id == first_port) {
expect(result).toEqual({
w: [100],
x: [first_port],
y: ["first port"],
z: [false],
});
} else if (updated.port_id == second_port) {
expect(result).toEqual({
w: [200],
x: [second_port],
y: ["second port"],
z: [true],
});
}
update_count++;
if (update_count === 2) {
_v.delete();
_t.delete();
view.delete();
table.delete();
done();
}
},
{ mode: "row" },
);
const first_port = get_random_int(0, 10);
let second_port = get_random_int(0, 10);
while (second_port === first_port) {
second_port = get_random_int(0, 10);
}
await table.update(
{
w: [100],
x: [first_port],
y: ["first port"],
z: [false],
},
{ port_id: first_port },
);
await table.update(
{
w: [200],
x: [second_port],
y: ["second port"],
z: [true],
},
{ port_id: second_port },
);
},
);
});
test.describe("Cross-table port operations", function () {
it_old_behavior(
"Should allow a client-server round trip without loops",
async function (done) {
const client_table = await perspective.table(data);
const server_table = await perspective.table(data);
const client_port = await client_table.make_port();
// "get rid" of a random number of ports by creating them
for (let i = 0; i < get_random_int(5, 20); i++) {
await server_table.make_port();
}
const server_port = await server_table.make_port();
expect(client_port).toBeLessThan(server_port);
const client_view = await client_table.view();
let CLIENT_TO_SERVER_UPDATES = 0;
client_view.on_update(
async (updated) => {
if (updated.port_id === client_port) {
await server_table.update(updated.delta, {
port_id: server_port,
});
CLIENT_TO_SERVER_UPDATES++;
} else {
// Should not pass forward that update, and break
// the chain.
expect(CLIENT_TO_SERVER_UPDATES).toEqual(1);
}
},
{ mode: "row" },
);
const server_view = await server_table.view();
server_view.on_update(
async (updated) => {
if (updated.port_id !== server_port) {
await client_table.update(updated.delta);
} else {
// update is trapped in this state - assert correct
// state and exit the test.
expect(await client_table.size()).toEqual(8);
expect(await server_table.size()).toEqual(8);
client_view.delete();
server_view.delete();
client_table.delete();
server_table.delete();
done();
}
},
{ mode: "row" },
);
// this should go to the server and round trip back to the
// client, but stop when it gets to the client again.
await client_table.update(data, { port_id: client_port });
},
);
it_old_behavior(
"Should allow multi client-server round trips without loops",
async function (done) {
const client_table = await perspective.table(data, {
index: "w",
});
const client_table2 = await perspective.table(data, {
index: "w",
});
const server_table = await perspective.table(data, {
index: "w",
});
const client_port = await client_table.make_port();
const client_port2 = await client_table2.make_port();
// "get rid" of a random number of ports by creating them
for (let i = 0; i < get_random_int(5, 20); i++) {
await server_table.make_port();
}
const server_port = await server_table.make_port();
const server_port2 = await server_table.make_port();
expect(client_port).toBeLessThan(server_port);
expect(client_port2).toBeLessThan(server_port);
expect(client_port).toBeLessThan(server_port2);
expect(client_port2).toBeLessThan(server_port2);
// port numbers can be equal between clients
expect(client_port2).toEqual(client_port);
const client_view = await client_table.view();
const client_view2 = await client_table2.view();
client_view.on_update(
async (updated) => {
// client1 and server handlers are as minimal as can be
if (updated.port_id === client_port) {
await server_table.update(updated.delta, {
port_id: server_port,
});
}
},
{ mode: "row" },
);
client_view2.on_update(
async (updated) => {
// client2 only reads updates from other clients
const results = await client_view2.to_columns();
expect(results).toEqual({
w: [1.5, 2.5, 3.5, 4.5],
x: [1, 2, 300, 4],
y: ["a", "b", "ccc", "d"],
z: [true, false, true, false],
});
expect(await client_view.to_columns()).toEqual(
results,
);
expect(await server_view.to_columns()).toEqual(
results,
);
client_view.delete();
client_view2.delete();
server_view.delete();
client_table.delete();
client_table2.delete();
server_table.delete();
done();
},
{ mode: "row" },
);
const server_view = await server_table.view();
// Simulate multiple connections to the server
server_view.on_update(
async (updated) => {
if (updated.port_id !== server_port) {
await client_table.update(updated.delta);
}
},
{ mode: "row" },
);
server_view.on_update(
async (updated) => {
if (updated.port_id !== server_port2) {
await client_table2.update(updated.delta);
}
},
{ mode: "row" },
);
// this should go to the server and round trip back to the
// client, but stop when it gets to the client again. It should
// reflect on the client2 table though.
await client_table.update(
[
{
w: 3.5,
x: 300,
y: "ccc",
},
],
{ port_id: client_port },
);
},
);
});
});
})(perspective);