Restructure to monorepo (#6907)

* wip: restructure to monorepo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor create-release-pr to a package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* github workflow fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix typo

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* add webpack-env types to core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix github workflows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* refactor/fix integration tests

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* lint fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* yarn run dev

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* eslint settings for vscode

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* move templates to right package

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* open-lens build fixes

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* integration test fix

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix nx task dependencies

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* use bash shell for unit tests in test workflow

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix test:unit for windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix win-ca webpack error in open-lens

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* fix build:app on windows

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove ELECTRON_BUILDER_EXTRA_ARGS

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* sync src/ from master

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* remove Makefile from core

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
1 parent 0066ca4ea2
commit 2657df2293
3321 files changed
+28699 -11447

No files matched your search

@@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getClusterIdFromHost } from "../cluster-id-url-parsing";
describe("getClusterIdFromHost", () => {
const clusterFakeId = "fe540901-0bd6-4f6c-b472-bce1559d7c4a";
it("should return undefined for non cluster frame hosts", () => {
expect(getClusterIdFromHost("lens.app:45345")).toBeUndefined();
});
it("should return ClusterId for cluster frame hosts", () => {
expect(getClusterIdFromHost(`${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
});
it("should return ClusterId for cluster frame hosts with additional subdomains", () => {
expect(getClusterIdFromHost(`abc.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
expect(getClusterIdFromHost(`abc.def.ghi.jkl.mno.pqr.stu.vwx.yz.${clusterFakeId}.lens.app:59110`)).toBe(clusterFakeId);
});
});
@@ -0,0 +1,128 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { bytesToUnits, unitsToBytes } from "../convertMemory";
describe("unitsToBytes", () => {
it("without any units, just parse as float", () => {
expect(unitsToBytes("1234")).toBe(1234);
});
it("should parse with B suffix", () => {
expect(unitsToBytes("1234B")).toBe(1234);
});
it("should parse with Ki suffix", () => {
expect(unitsToBytes("1234Ki")).toBe(1234 * (1024));
});
it("should parse with Ki suffix as the same as KiB", () => {
expect(unitsToBytes("1234Ki")).toBe(unitsToBytes("1234KiB"));
});
it("should parse with Mi suffix", () => {
expect(unitsToBytes("1234Mi")).toBe(1234 * (1024 ** 2));
});
it("should parse with Mi suffix as the same as MiB", () => {
expect(unitsToBytes("1234Mi")).toBe(unitsToBytes("1234MiB"));
});
it("should parse with Gi suffix", () => {
expect(unitsToBytes("1234Gi")).toBe(1234 * (1024 ** 3));
});
it("should parse with Gi suffix as the same as GiB", () => {
expect(unitsToBytes("1234Gi")).toBe(unitsToBytes("1234GiB"));
});
it("should parse with Ti suffix", () => {
expect(unitsToBytes("1234Ti")).toBe(1234 * (1024 ** 4));
});
it("should parse with Ti suffix as the same as TiB", () => {
expect(unitsToBytes("1234Ti")).toBe(unitsToBytes("1234TiB"));
});
it("should parse with Pi suffix", () => {
expect(unitsToBytes("1234Pi")).toBe(1234 * (1024 ** 5));
});
it("should parse with Pi suffix as the same as PiB", () => {
expect(unitsToBytes("1234Pi")).toBe(unitsToBytes("1234PiB"));
});
it("given unrelated data, return NaN", () => {
expect(unitsToBytes("I am not a number")).toBeNaN();
});
it("given unrelated data, but has number, return that", () => {
expect(unitsToBytes("I am not a number, but this is 0.1")).toBe(0.1);
});
});
describe("bytesToUnits", () => {
it("should return N/A for invalid bytes", () => {
expect(bytesToUnits(-1)).toBe("N/A");
expect(bytesToUnits(Infinity)).toBe("N/A");
expect(bytesToUnits(NaN)).toBe("N/A");
});
it("given a number within the magnitude of 0..124, format with B", () => {
expect(bytesToUnits(100)).toBe("100.0B");
});
it("given a number within the magnitude of 1024..1024^2, format with KiB", () => {
expect(bytesToUnits(1024)).toBe("1.0KiB");
expect(bytesToUnits(2048)).toBe("2.0KiB");
expect(bytesToUnits(1900)).toBe("1.9KiB");
expect(bytesToUnits(50*1024 + 1)).toBe("50.0KiB");
});
it("given a number within the magnitude of 1024^2..1024^3, format with MiB", () => {
expect(bytesToUnits(1024**2)).toBe("1.0MiB");
expect(bytesToUnits(2048**2)).toBe("4.0MiB");
expect(bytesToUnits(1900 * 1024)).toBe("1.9MiB");
expect(bytesToUnits(50*(1024 ** 2) + 1)).toBe("50.0MiB");
});
it("given a number within the magnitude of 1024^3..1024^4, format with GiB", () => {
expect(bytesToUnits(1024**3)).toBe("1.0GiB");
expect(bytesToUnits(2048**3)).toBe("8.0GiB");
expect(bytesToUnits(1900 * 1024 ** 2)).toBe("1.9GiB");
expect(bytesToUnits(50*(1024 ** 3) + 1)).toBe("50.0GiB");
});
it("given a number within the magnitude of 1024^4..1024^5, format with TiB", () => {
expect(bytesToUnits(1024**4)).toBe("1.0TiB");
expect(bytesToUnits(2048**4)).toBe("16.0TiB");
expect(bytesToUnits(1900 * 1024 ** 3)).toBe("1.9TiB");
expect(bytesToUnits(50*(1024 ** 4) + 1)).toBe("50.0TiB");
});
it("given a number within the magnitude of 1024^5..1024^6, format with PiB", () => {
expect(bytesToUnits(1024**5)).toBe("1.0PiB");
expect(bytesToUnits(2048**5)).toBe("32.0PiB");
expect(bytesToUnits(1900 * 1024 ** 4)).toBe("1.9PiB");
expect(bytesToUnits(50*(1024 ** 5) + 1)).toBe("50.0PiB");
});
});
describe("bytesToUnits -> unitsToBytes", () => {
it("given an input, round trip to the same value, given enough precision", () => {
expect(unitsToBytes(bytesToUnits(123))).toBe(123);
expect(unitsToBytes(bytesToUnits(1024**0 + 1, { precision: 2 }))).toBe(1024**0 + 1);
expect(unitsToBytes(bytesToUnits(1024**1 + 2, { precision: 3 }))).toBe(1024**1 + 2);
expect(unitsToBytes(bytesToUnits(1024**2 + 3, { precision: 6 }))).toBe(1024**2 + 3);
expect(unitsToBytes(bytesToUnits(1024**3 + 4, { precision: 9 }))).toBe(1024**3 + 4);
expect(unitsToBytes(bytesToUnits(1024**4 + 5, { precision: 12 }))).toBe(1024**4 + 5);
expect(unitsToBytes(bytesToUnits(1024**5 + 6, { precision: 16 }))).toBe(1024**5 + 6);
expect(unitsToBytes(bytesToUnits(1024**6 + 7, { precision: 20 }))).toBe(1024**6 + 7);
});
it("given an invalid input, round trips to NaN", () => {
expect(unitsToBytes(bytesToUnits(-1))).toBeNaN();
});
});
@@ -0,0 +1,94 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import moment from "moment";
import { formatDuration } from "../formatDuration";
const second = 1000;
const minute = 60 * second;
const hour = 60 * minute;
const day = 24 * hour;
const year = 365 * day;
describe("human format durations", () => {
test("small duration should output something", () => {
expect(formatDuration(1)).toBe("0s");
expect(formatDuration(3)).toBe("0s");
});
test("returns seconds for duration under 1 min", () => {
const res = formatDuration(8 * second);
expect(res).toBe("8s");
});
test("zero duration should output something", () => {
expect(formatDuration(0)).toBe("0s");
});
describe("when compact is true", () => {
test("duration under 3 hours return minutes", () => {
const res = formatDuration(1 * hour + 35 * minute);
expect(res).toBe("95m");
});
test("duration under 8 hours return hours and minutes", () => {
const res = formatDuration(6 * hour + 15 * minute + 20 * second);
expect(res).toBe("6h15m");
});
test("duration under 48 hours return hours", () => {
const res = formatDuration(1 * day + 4 * hour + 15 * minute);
expect(res).toBe("28h");
});
test("duration under 2 years return days", () => {
const res = formatDuration(400 * day + 4 * hour + 15 * minute);
expect(res).toBe("400d");
});
test("durations less than 8 years returns years and days", () => {
const timeValue = new Date(2020, 0, 10, 12, 0, 0, 0).getTime() - new Date(2018, 0, 4, 12, 0, 0, 0).getTime();
const res = formatDuration(timeValue);
expect(res).toBe("2y5d");
});
test("durations more than 8 years returns years", () => {
const timeValue = Date.now() - new Date(moment().subtract(9, "years").subtract(5, "days").toDate()).getTime();
const res = formatDuration(timeValue);
expect(res).toBe("9y");
});
test("durations more than 8 years returns years", () => {
const res = formatDuration(10 * year + 25 * day);
expect(res).toBe("10y");
});
test("durations shouldn't include zero magnitude parts", () => {
const zeroSeconds = formatDuration(8 * minute);
expect(zeroSeconds).toBe("8m");
const zeroMinutes = formatDuration(8 * hour + 15 * minute);
expect(zeroMinutes).toBe("8h");
const zeroHours = formatDuration(6 * day + 2 * minute);
expect(zeroHours).toBe("6d");
});
});
});
@@ -0,0 +1,512 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { HashSet, ObservableHashSet } from "../hash-set";
describe("ObservableHashSet<T>", () => {
it("should not throw on creation", () => {
expect(() => new ObservableHashSet<{ a: number }>([], item => item.a.toString())).not.toThrowError();
});
it("should be initializable", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.size).toBe(4);
});
it("has should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.has({ a: 1 })).toBe(true);
expect(res.has({ a: 5 })).toBe(false);
});
it("forEach should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
let a = 1;
res.forEach((item) => {
expect(item.a).toEqual(a++);
});
});
it("delete should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.has({ a: 1 })).toBe(true);
expect(res.delete({ a: 1 })).toBe(true);
expect(res.has({ a: 1 })).toBe(false);
expect(res.has({ a: 5 })).toBe(false);
expect(res.delete({ a: 5 })).toBe(false);
expect(res.has({ a: 5 })).toBe(false);
});
it("toggle should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.has({ a: 1 })).toBe(true);
res.toggle({ a: 1 });
expect(res.has({ a: 1 })).toBe(false);
expect(res.has({ a: 6 })).toBe(false);
res.toggle({ a: 6 });
expect(res.has({ a: 6 })).toBe(true);
});
it("add should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.has({ a: 6 })).toBe(false);
res.add({ a: 6 });
expect(res.has({ a: 6 })).toBe(true);
});
it("add should treat the hash to be the same as equality", () => {
const res = new ObservableHashSet([
{ a: 1, foobar: "hello" },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.has({ a: 1 })).toBe(true);
res.add({ a: 1, foobar: "goodbye" });
expect(res.has({ a: 1 })).toBe(true);
});
it("clear should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.size).toBe(4);
res.clear();
expect(res.size).toBe(0);
});
it("replace should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.size).toBe(4);
res.replace([{ a: 13 }]);
expect(res.size).toBe(1);
expect(res.has({ a: 1 })).toBe(false);
expect(res.has({ a: 13 })).toBe(true);
});
it("toJSON should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.toJSON()).toStrictEqual([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
]);
});
it("values should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
const iter = res.values();
expect(iter.next()).toStrictEqual({
value: { a: 1 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 2 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 3 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 4 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: undefined,
done: true,
});
});
it("keys should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
const iter = res.keys();
expect(iter.next()).toStrictEqual({
value: { a: 1 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 2 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 3 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 4 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: undefined,
done: true,
});
});
it("entries should work as expected", () => {
const res = new ObservableHashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
const iter = res.entries();
expect(iter.next()).toStrictEqual({
value: [{ a: 1 }, { a: 1 }],
done: false,
});
expect(iter.next()).toStrictEqual({
value: [{ a: 2 }, { a: 2 }],
done: false,
});
expect(iter.next()).toStrictEqual({
value: [{ a: 3 }, { a: 3 }],
done: false,
});
expect(iter.next()).toStrictEqual({
value: [{ a: 4 }, { a: 4 }],
done: false,
});
expect(iter.next()).toStrictEqual({
value: undefined,
done: true,
});
});
});
describe("HashSet<T>", () => {
it("should not throw on creation", () => {
expect(() => new HashSet<{ a: number }>([], item => item.a.toString())).not.toThrowError();
});
it("should be initializable", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.size).toBe(4);
});
it("has should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.has({ a: 1 })).toBe(true);
expect(res.has({ a: 5 })).toBe(false);
});
it("forEach should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
let a = 1;
res.forEach((item) => {
expect(item.a).toEqual(a++);
});
});
it("delete should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.has({ a: 1 })).toBe(true);
expect(res.delete({ a: 1 })).toBe(true);
expect(res.has({ a: 1 })).toBe(false);
expect(res.has({ a: 5 })).toBe(false);
expect(res.delete({ a: 5 })).toBe(false);
expect(res.has({ a: 5 })).toBe(false);
});
it("toggle should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.has({ a: 1 })).toBe(true);
res.toggle({ a: 1 });
expect(res.has({ a: 1 })).toBe(false);
expect(res.has({ a: 6 })).toBe(false);
res.toggle({ a: 6 });
expect(res.has({ a: 6 })).toBe(true);
});
it("add should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.has({ a: 6 })).toBe(false);
res.add({ a: 6 });
expect(res.has({ a: 6 })).toBe(true);
});
it("add should treat the hash to be the same as equality", () => {
const res = new HashSet([
{ a: 1, foobar: "hello" },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.has({ a: 1 })).toBe(true);
res.add({ a: 1, foobar: "goodbye" });
expect(res.has({ a: 1 })).toBe(true);
});
it("clear should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.size).toBe(4);
res.clear();
expect(res.size).toBe(0);
});
it("replace should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.size).toBe(4);
res.replace([{ a: 13 }]);
expect(res.size).toBe(1);
expect(res.has({ a: 1 })).toBe(false);
expect(res.has({ a: 13 })).toBe(true);
});
it("toJSON should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
expect(res.toJSON()).toStrictEqual([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
]);
});
it("values should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
const iter = res.values();
expect(iter.next()).toStrictEqual({
value: { a: 1 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 2 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 3 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 4 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: undefined,
done: true,
});
});
it("keys should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
const iter = res.keys();
expect(iter.next()).toStrictEqual({
value: { a: 1 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 2 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 3 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: { a: 4 },
done: false,
});
expect(iter.next()).toStrictEqual({
value: undefined,
done: true,
});
});
it("entries should work as expected", () => {
const res = new HashSet([
{ a: 1 },
{ a: 2 },
{ a: 3 },
{ a: 4 },
], item => item.a.toString());
const iter = res.entries();
expect(iter.next()).toStrictEqual({
value: [{ a: 1 }, { a: 1 }],
done: false,
});
expect(iter.next()).toStrictEqual({
value: [{ a: 2 }, { a: 2 }],
done: false,
});
expect(iter.next()).toStrictEqual({
value: [{ a: 3 }, { a: 3 }],
done: false,
});
expect(iter.next()).toStrictEqual({
value: [{ a: 4 }, { a: 4 }],
done: false,
});
expect(iter.next()).toStrictEqual({
value: undefined,
done: true,
});
});
});
@@ -0,0 +1,68 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { join, nth, reduce, concat } from "../iter";
describe("iter", () => {
describe("reduce", () => {
it("can reduce a value", () => {
expect(reduce([1, 2, 3], (acc: number[], current: number) => [current, ...acc], [0])).toEqual([3, 2, 1, 0]);
});
it("can reduce an empty iterable", () => {
expect(reduce([], (acc: number[], current: number) => [acc[0] + current], [])).toEqual([]);
});
});
describe("join", () => {
it("should not prefix the output by the seperator", () => {
expect(join(["a", "b", "c"].values(), " ")).toBe("a b c");
});
it("should return empty string if iterator is empty", () => {
expect(join([].values(), " ")).toBe("");
});
it("should return just first entry if iterator is of size 1", () => {
expect(join(["d"].values(), " ")).toBe("d");
});
});
describe("nth", () => {
it("should return undefined past the end of the iterator", () => {
expect(nth(["a"], 123)).toBeUndefined();
});
it("should by 0-indexing the index", () => {
expect(nth(["a", "b"], 0)).toBe("a");
});
});
describe("concat", () => {
it("should yield undefined for empty args", () => {
const iter = concat();
expect(iter.next()).toEqual({ done: true });
});
it("should yield undefined for only empty args", () => {
const iter = concat([].values(), [].values(), [].values(), [].values());
expect(iter.next()).toEqual({ done: true });
});
it("should yield all of the first and then all of the second", () => {
const iter = concat([1, 2, 3].values(), [4, 5, 6].values());
expect(iter.next()).toEqual({ done: false, value: 1 });
expect(iter.next()).toEqual({ done: false, value: 2 });
expect(iter.next()).toEqual({ done: false, value: 3 });
expect(iter.next()).toEqual({ done: false, value: 4 });
expect(iter.next()).toEqual({ done: false, value: 5 });
expect(iter.next()).toEqual({ done: false, value: 6 });
expect(iter.next()).toEqual({ done: true });
});
});
});
@@ -0,0 +1,39 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { nFircate } from "../n-fircate";
describe("nFircate", () => {
it("should produce an empty array if no parts are provided", () => {
expect(nFircate([{ a: 1 }, { a: 2 }], "a", []).length).toBe(0);
});
it("should ignore non-matching parts", () => {
const res = nFircate([{ a: 1 }, { a: 2 }], "a", [1]);
expect(res.length).toBe(1);
expect(res[0].length).toBe(1);
});
it("should include all matching parts in each type", () => {
const res = nFircate([{ a: 1, b: "a" }, { a: 2, b: "b" }, { a: 1, b: "c" }], "a", [1, 2]);
expect(res.length).toBe(2);
expect(res[0].length).toBe(2);
expect(res[0][0].b).toBe("a");
expect(res[0][1].b).toBe("c");
expect(res[1].length).toBe(1);
expect(res[1][0].b).toBe("b");
});
it("should throw a type error if the same part is provided more than once", () => {
try {
nFircate([{ a: 1, b: "a" }, { a: 2, b: "b" }, { a: 1, b: "c" }], "a", [1, 2, 1]);
fail("Expected error");
} catch (error) {
expect(error).toBeInstanceOf(TypeError);
}
});
});
@@ -0,0 +1,137 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import path from "path";
import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable";
import getDirnameOfPathInjectable from "../../path/get-dirname.injectable";
import type { IsLogicalChildPath } from "../../path/is-logical-child-path.injectable";
import isLogicalChildPathInjectable from "../../path/is-logical-child-path.injectable";
describe("isLogicalChildPath", () => {
let di: DiContainer;
let isLogicalChildPath: IsLogicalChildPath;
beforeEach(() => {
di = getDiForUnitTesting();
});
describe("when using win32 paths", () => {
beforeEach(() => {
di.override(getAbsolutePathInjectable, () => path.win32.resolve);
di.override(getDirnameOfPathInjectable, () => path.win32.dirname);
isLogicalChildPath = di.inject(isLogicalChildPathInjectable);
});
it.each([
{
parentPath: "C:\\Foo",
testPath: "C:\\Foo\\Bar",
expected: true,
},
{
parentPath: "C:\\Foo",
testPath: "C:\\Bar",
expected: false,
},
{
parentPath: "C:\\Foo",
testPath: "C:/Bar",
expected: false,
},
{
parentPath: "C:\\Foo",
testPath: "C:/Foo/Bar",
expected: true,
},
{
parentPath: "C:\\Foo",
testPath: "D:\\Foo\\Bar",
expected: false,
},
])("test %#", (testData) => {
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
});
});
describe("when using posix paths", () => {
beforeEach(() => {
di.override(getAbsolutePathInjectable, () => path.posix.resolve);
di.override(getDirnameOfPathInjectable, () => path.posix.dirname);
isLogicalChildPath = di.inject(isLogicalChildPathInjectable);
});
it.each([
{
parentPath: "/foo",
testPath: "/foo",
expected: false,
},
{
parentPath: "/foo",
testPath: "/bar",
expected: false,
},
{
parentPath: "/foo",
testPath: "/foobar",
expected: false,
},
{
parentPath: "/foo",
testPath: "/foo/bar",
expected: true,
},
{
parentPath: "/foo",
testPath: "/foo/../bar",
expected: false,
},
{
parentPath: "/foo",
testPath: "/foo/./bar",
expected: true,
},
{
parentPath: "/foo",
testPath: "/foo/.bar",
expected: true,
},
{
parentPath: "/foo",
testPath: "/foo/..bar",
expected: true,
},
{
parentPath: "/foo",
testPath: "/foo/...bar",
expected: true,
},
{
parentPath: "/foo",
testPath: "/foo/..\\.bar",
expected: true,
},
{
parentPath: "/bar/../foo",
testPath: "/foo/bar",
expected: true,
},
{
parentPath: "/foo",
testPath: "/foo/\\bar",
expected: true,
},
{
parentPath: "/foo",
testPath: "./bar",
expected: false,
},
])("test %#", (testData) => {
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
});
});
});
@@ -0,0 +1,66 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { bifurcateArray, splitArray } from "../splitArray";
describe("split array on element tests", () => {
it("empty array", () => {
expect(splitArray([], 10)).toStrictEqual([[], [], false]);
});
it("one element, not in array", () => {
expect(splitArray([1], 10)).toStrictEqual([[1], [], false]);
});
it("ten elements, not in array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], [], false]);
});
it("one elements, in array", () => {
expect(splitArray([1], 1)).toStrictEqual([[], [], true]);
});
it("ten elements, in front array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0)).toStrictEqual([[], [1, 2, 3, 4, 5, 6, 7, 8, 9], true]);
});
it("ten elements, in middle array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4)).toStrictEqual([[0, 1, 2, 3], [5, 6, 7, 8, 9], true]);
});
it("ten elements, in end array", () => {
expect(splitArray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 9)).toStrictEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8], [], true]);
});
});
describe("bifurcateArray", () => {
it("should return tuple of empty arrays from empty array", () => {
const [left, right] = bifurcateArray([], () => true);
expect(left).toStrictEqual([]);
expect(right).toStrictEqual([]);
});
it("should return all true condition returning items in the right array", () => {
const [left, right] = bifurcateArray([1, 2, 3], () => true);
expect(left).toStrictEqual([]);
expect(right).toStrictEqual([1, 2, 3]);
});
it("should return all false condition returning items in the right array", () => {
const [left, right] = bifurcateArray([1, 2, 3], () => false);
expect(left).toStrictEqual([1, 2, 3]);
expect(right).toStrictEqual([]);
});
it("should split array as specified", () => {
const [left, right] = bifurcateArray([1, 2, 3], (i) => Boolean(i % 2));
expect(left).toStrictEqual([2]);
expect(right).toStrictEqual([1, 3]);
});
});
@@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isObservable, observable } from "mobx";
import { toJS } from "../toJS";
describe("utils/toJS(data: any)", () => {
const y = { y: 2 };
const data = observable({ x: 1, y }, {}, {
deep: false, // this will keep ref to "y"
});
const data2 = {
x: 1, // partially observable
y: observable(y),
};
test("converts mobx-observable to corresponding js struct with links preserving", () => {
expect(toJS(data).y).toBe(y);
expect(isObservable(toJS(data).y)).toBeFalsy();
});
test("converts partially observable js struct", () => {
expect(toJS(data2).y).not.toBe(y);
expect(toJS(data2).y).toEqual(y);
expect(isObservable(toJS(data2).y)).toBeFalsy();
});
});
@@ -0,0 +1,43 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { tuple } from "../../utils";
describe("tuple tests", () => {
describe("zip()", () => {
it("should yield 0 times and return 1 tuple of empty arrays when given empty array", () => {
expect(tuple.zip([]).next()).toEqual({
done: true,
value: [[]],
});
});
it("should yield 1 times and return 2 tuple of empty arrays when given one element array tuples", () => {
const i = tuple.zip([1], [2]);
expect(i.next()).toEqual({
done: false,
value: [1, 2],
});
expect(i.next()).toEqual({
done: true,
value: [[], []],
});
});
it("should yield 1 times and return 2 tuple of partial arrays when given one element array tuples", () => {
const i = tuple.zip([1], [2, 3]);
expect(i.next()).toEqual({
done: false,
value: [1, 2],
});
expect(i.next()).toEqual({
done: true,
value: [[], [3]],
});
});
});
});
@@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import { unionPATHs } from "../union-env-path";
describe("unionPATHs", () => {
it("return the same path if given only one with no double delimiters", () => {
expect(unionPATHs(`/bin/bar${path.delimiter}/usr/bin`)).toBe(`/bin/bar${path.delimiter}/usr/bin`);
});
it("return equivalent path if given only one with no double delimiters", () => {
expect(unionPATHs(`/bin/bar${path.delimiter}${path.delimiter}/usr/bin`)).toBe(`/bin/bar${path.delimiter}/usr/bin`);
});
it("should remove duplicate entries, appending non duplicates in order received", () => {
expect(unionPATHs(
`/bin/bar${path.delimiter}/usr/bin`,
`/bin/bar${path.delimiter}/usr/lens/bat`,
)).toBe(`/bin/bar${path.delimiter}/usr/bin${path.delimiter}/usr/lens/bat`);
});
it("should remove duplicate entries, appending non duplicates in order received, 3", () => {
expect(unionPATHs(
`/bin/bar${path.delimiter}/usr/bin`,
`/bin/bar${path.delimiter}/usr/lens/bat`,
`/usr/local/lens${path.delimiter}/usr/bin`,
)).toBe(`/bin/bar${path.delimiter}/usr/bin${path.delimiter}/usr/lens/bat${path.delimiter}/usr/local/lens`);
});
});
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import AbortController from "abort-controller";
/**
* This is like an `AbortController` but will also abort if the parent aborts,
* but won't make the parent abort if this aborts (single direction)
*/
export class WrappedAbortController extends AbortController {
constructor(parent?: AbortController | undefined) {
super();
parent?.signal.addEventListener("abort", () => {
this.abort();
});
}
}
export function setTimeoutFor(controller: AbortController, timeout: number): void {
const handle = setTimeout(() => controller.abort(), timeout);
controller.signal.addEventListener("abort", () => clearTimeout(handle));
}
@@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { addSeparator } from "./add-separator";
describe("add-separator", () => {
it("given multiple items, adds separators", () => {
const items = ["first", "second", "third"];
const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
expect(actual).toEqual([
"first",
"separator-between-first-and-second",
"second",
"separator-between-second-and-third",
"third",
]);
});
it("given multiple items including falsy ones, adds separators", () => {
const items = [false, undefined, null, NaN];
const actual = addSeparator((left, right) => `separator-between-${left}-and-${right}`, items);
expect(actual).toEqual([
false,
"separator-between-false-and-undefined",
undefined,
"separator-between-undefined-and-null",
null,
"separator-between-null-and-NaN",
NaN,
]);
});
it("given no items, does not add separator", () => {
const items: any[] = [];
const actual = addSeparator(() => "separator", items);
expect(actual).toEqual([]);
});
it("given one item, does not add separator", () => {
const items = ["first"];
const actual = addSeparator(() => "separator", items);
expect(actual).toEqual(["first"]);
});
});
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type GetSeparator<Item, Separator> = (left: Item, right: Item) => Separator;
export const addSeparator = <Item, Separator>(
getSeparator: GetSeparator<Item, Separator>,
items: Item[],
) => items.flatMap(toSeparatedTupleUsing(getSeparator));
const toSeparatedTupleUsing =
<Item, Separator>(getSeparator: GetSeparator<Item, Separator>) =>
(leftItem: Item, index: number, arr: Item[]) => {
const itemIsLast = arr.length === index + 1;
if (itemIsLast) {
return [leftItem];
}
const rightItem = arr[index + 1];
const separator = getSeparator(leftItem, rightItem);
return [leftItem, separator];
};
+13
View File
@@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* A inference typed version of `Array(length).fill(value)`
* @param length The number of entries
* @param value The value of each of the indices
*/
export function filled<T>(length: number, value: T): T[] {
return Array(length).fill(value);
}
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type AsyncResult<Response, Error = string> =
| (
Response extends void
? { callWasSuccessful: true; response?: undefined }
: { callWasSuccessful: true; response: Response }
)
| { callWasSuccessful: false; error: Error };
@@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Options } from "auto-bind";
import autoBindClass from "auto-bind";
import autoBindReactClass from "auto-bind/react";
import React from "react";
// Automatically bind methods to their class instance
export function autoBind<T extends object>(obj: T, opts?: Options): T {
if (obj instanceof React.Component) {
return autoBindReactClass(obj, opts);
}
return autoBindClass(obj, opts);
}
+26
View File
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Encode/decode utf-8 base64 string
import * as Base64 from "crypto-js/enc-base64";
import * as Utf8 from "crypto-js/enc-utf8";
/**
* Computes utf-8 from base64
* @param data A Base64 encoded string
* @returns The original utf-8 string
*/
export function decode(data: string): string {
return Base64.parse(data).toString(Utf8);
}
/**
* Computes base64 from utf-8
* @param data A normal string
* @returns A base64 encoded version
*/
export function encode(data: string): string {
return Utf8.parse(data).toString(Base64);
}
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import normalizedPlatformInjectable from "../vars/normalized-platform.injectable";
const binaryNameInjectable = getInjectable({
id: "binary-name",
instantiate: (di, binaryName) => {
const normalizedPlatform = di.inject(normalizedPlatformInjectable);
if (normalizedPlatform === "windows") {
return `${binaryName}.exe`;
}
return binaryName;
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, binaryName: string) => binaryName,
}),
});
export default binaryNameInjectable;
@@ -0,0 +1,58 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { compile } from "path-to-regexp";
import type { RouteProps } from "react-router";
import { isDefined } from "./type-narrowing";
export interface UrlRouteProps extends RouteProps {
path: string;
}
export interface URLParams<P extends object = {}, Q extends object = {}> {
params?: P;
query?: Q;
fragment?: string;
}
export function buildURL<P extends object = {}, Q extends object = {}>(path: string, { params, query, fragment }: URLParams<P, Q> = {}) {
const pathBuilder = compile(String(path));
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : "";
const parts = [
pathBuilder(params),
queryParams && `?${queryParams}`,
fragment && `#${fragment}`,
];
return parts.filter(isDefined).join("");
}
export function buildURLPositional<P extends object = {}, Q extends object = {}>(path: string) {
return function (params?: P, query?: Q, fragment?: string): string {
return buildURL(path, { params, query, fragment });
};
}
export type UrlParamsFor<Pathname extends string> =
Pathname extends `${string}/:${infer A}?/${infer Tail}`
? Partial<Record<A, string>> & UrlParamsFor<`/${Tail}`>
: Pathname extends `${string}/:${infer A}/${infer Tail}`
? Record<A, string> & UrlParamsFor<`/${Tail}`>
: Pathname extends `${string}/:${infer A}?`
? Partial<Record<A, string>>
: Pathname extends `${string}/:${infer A}`
? Record<A, string>
: {};
export interface UrlBuilder<Pathname extends string> {
compile(params: UrlParamsFor<Pathname>, query?: object, fragment?: string): string;
}
export function urlBuilderFor<Pathname extends string>(pathname: Pathname): UrlBuilder<Pathname> {
return {
compile: buildURLPositional(pathname),
};
}
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import joinPathsInjectable from "../path/join-paths.injectable";
import baseBundledBinariesDirectoryInjectable from "../vars/base-bundled-binaries-dir.injectable";
import binaryNameInjectable from "./binary-name.injectable";
const bundledBinaryPathInjectable = getInjectable({
id: "bundled-binary-path",
instantiate: (di, name) => {
const joinPaths = di.inject(joinPathsInjectable);
const binaryName = di.inject(binaryNameInjectable, name);
const baseBundledBinariesDirectory = di.inject(baseBundledBinariesDirectoryInjectable);
return joinPaths(baseBundledBinariesDirectory, binaryName);
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, binaryName: string) => binaryName,
}),
});
export default bundledBinaryPathInjectable;
@@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Convert object's keys to camelCase format
import { camelCase } from "lodash";
import type { SingleOrMany } from "./types";
import { isObject, isString } from "./type-narrowing";
import * as object from "./objects";
export function toCamelCase<T extends Record<string, unknown>[]>(obj: T): T;
export function toCamelCase<T extends Record<string, unknown>>(obj: T): T;
export function toCamelCase(obj: SingleOrMany<Record<string, unknown> | unknown>): SingleOrMany<Record<string, unknown> | unknown> {
if (Array.isArray(obj)) {
return obj.map(toCamelCase);
}
if (isObject(obj)) {
return object.fromEntries(
object.entries(obj)
.filter((pair): pair is [string, unknown] => isString(pair[0]))
.map(([key, value]) => [camelCase(key), isObject(value) ? toCamelCase(value) : value]),
);
}
return obj;
}
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface Channel<MessageTemplate = void, ReturnTemplate = void> {
id: string;
_messageTemplate?: MessageTemplate;
_returnTemplate?: ReturnTemplate;
}
@@ -0,0 +1,231 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import type { SendMessageToChannel } from "./message-to-channel-injection-token";
import { sendMessageToChannelInjectionToken } from "./message-to-channel-injection-token";
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
import type { MessageChannel } from "./message-channel-listener-injection-token";
import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
import type { RequestFromChannel } from "./request-from-channel-injection-token";
import { requestFromChannelInjectionToken } from "./request-from-channel-injection-token";
import type { RequestChannel } from "./request-channel-listener-injection-token";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getPromiseStatus } from "../../test-utils/get-promise-status";
import { runInAction } from "mobx";
import type { RequestChannelHandler } from "../../../main/utils/channel/channel-listeners/listener-tokens";
import { getRequestChannelListenerInjectable } from "../../../main/utils/channel/channel-listeners/listener-tokens";
type TestMessageChannel = MessageChannel<string>;
type TestRequestChannel = RequestChannel<string, string>;
describe("channel", () => {
describe("messaging from main to renderer, given listener for channel in a window and application has started", () => {
let messageListenerInWindowMock: jest.Mock;
let mainDi: DiContainer;
let messageToChannel: SendMessageToChannel;
let builder: ApplicationBuilder;
beforeEach(async () => {
builder = getApplicationBuilder();
messageListenerInWindowMock = jest.fn();
const testChannelListenerInTestWindowInjectable = getInjectable({
id: "test-channel-listener-in-test-window",
instantiate: () => ({
channel: testMessageChannel,
handler: messageListenerInWindowMock,
}),
injectionToken: messageChannelListenerInjectionToken,
});
builder.beforeWindowStart((windowDi) => {
runInAction(() => {
windowDi.register(testChannelListenerInTestWindowInjectable);
});
});
mainDi = builder.mainDi;
await builder.startHidden();
messageToChannel = mainDi.inject(sendMessageToChannelInjectionToken);
});
describe("given window is started", () => {
let someWindowFake: LensWindow;
beforeEach(async () => {
someWindowFake = builder.applicationWindow.create("some-window");
await someWindowFake.start();
});
it("when sending message, triggers listener in window", () => {
messageToChannel(testMessageChannel, "some-message");
expect(messageListenerInWindowMock).toHaveBeenCalledWith("some-message");
});
it("given window is hidden, when sending message, does not trigger listener in window", () => {
someWindowFake.close();
messageToChannel(testMessageChannel, "some-message");
expect(messageListenerInWindowMock).not.toHaveBeenCalled();
});
});
it("given multiple started windows, when sending message, triggers listeners in all windows", async () => {
const someWindowFake = builder.applicationWindow.create("some-window");
const someOtherWindowFake = builder.applicationWindow.create("some-other-window");
await someWindowFake.start();
await someOtherWindowFake.start();
messageToChannel(testMessageChannel, "some-message");
expect(messageListenerInWindowMock.mock.calls).toEqual([
["some-message"],
["some-message"],
]);
});
});
describe("messaging from renderer to main, given listener for channel in a main and application has started", () => {
let messageListenerInMainMock: jest.Mock;
let messageToChannel: SendMessageToChannel;
beforeEach(async () => {
const applicationBuilder = getApplicationBuilder();
messageListenerInMainMock = jest.fn();
const testChannelListenerInMainInjectable = getInjectable({
id: "test-channel-listener-in-main",
instantiate: () => ({
channel: testMessageChannel,
handler: messageListenerInMainMock,
}),
injectionToken: messageChannelListenerInjectionToken,
});
applicationBuilder.beforeApplicationStart((mainDi) => {
runInAction(() => {
mainDi.register(testChannelListenerInMainInjectable);
});
});
await applicationBuilder.render();
const windowDi = applicationBuilder.applicationWindow.only.di;
messageToChannel = windowDi.inject(sendMessageToChannelInjectionToken);
});
it("when sending message, triggers listener in main", () => {
messageToChannel(testMessageChannel, "some-message");
expect(messageListenerInMainMock).toHaveBeenCalledWith("some-message");
});
});
describe("requesting from main in renderer, given listener for channel in a main and application has started", () => {
let requestListenerInMainMock: AsyncFnMock<RequestChannelHandler<TestRequestChannel>>;
let requestFromChannel: RequestFromChannel;
beforeEach(async () => {
const applicationBuilder = getApplicationBuilder();
requestListenerInMainMock = asyncFn();
const testChannelListenerInMainInjectable = getRequestChannelListenerInjectable({
channel: testRequestChannel,
handler: () => requestListenerInMainMock,
});
applicationBuilder.beforeApplicationStart((mainDi) => {
runInAction(() => {
mainDi.register(testChannelListenerInMainInjectable);
});
});
await applicationBuilder.render();
const windowDi = applicationBuilder.applicationWindow.only.di;
requestFromChannel = windowDi.inject(
requestFromChannelInjectionToken,
);
});
describe("when requesting from channel", () => {
let actualPromise: Promise<string>;
beforeEach(() => {
actualPromise = requestFromChannel(testRequestChannel, "some-request");
});
it("triggers listener in main", () => {
expect(requestListenerInMainMock).toHaveBeenCalledWith("some-request");
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("when main resolves with response, resolves with response", async () => {
await requestListenerInMainMock.resolve("some-response");
const actual = await actualPromise;
expect(actual).toBe("some-response");
});
});
});
it("when registering multiple handlers for the same channel, throws", async () => {
const applicationBuilder = getApplicationBuilder();
const testChannelListenerInMainInjectable = getRequestChannelListenerInjectable({
channel: testRequestChannel,
handler: () => () => "some-value",
});
const testChannelListenerInMain2Injectable = getRequestChannelListenerInjectable({
channel: testRequestChannel,
handler: () => () => "some-other-value",
});
testChannelListenerInMain2Injectable.id += "2";
applicationBuilder.beforeApplicationStart((mainDi) => {
runInAction(() => {
mainDi.register(testChannelListenerInMainInjectable);
mainDi.register(testChannelListenerInMain2Injectable);
});
});
await expect(applicationBuilder.render()).rejects.toThrow('Tried to register a multiple channel handlers for "some-request-channel-id", only one handler is supported for a request channel.');
});
});
const testMessageChannel: TestMessageChannel = {
id: "some-message-channel-id",
};
const testRequestChannel: TestRequestChannel = {
id: "some-request-channel-id",
};
@@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { Disposer } from "../disposer";
import type { MessageChannel, MessageChannelListener } from "./message-channel-listener-injection-token";
export type EnlistMessageChannelListener = (listener: MessageChannelListener<MessageChannel<unknown>>) => Disposer;
export const enlistMessageChannelListenerInjectionToken = getInjectionToken<EnlistMessageChannelListener>({
id: "enlist-message-channel-listener",
});
@@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RequestChannel } from "./request-channel-listener-injection-token";
export const getRequestChannel = <Request, Response>(id: string): RequestChannel<Request, Response> => ({
id,
});
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { getStartableStoppable } from "../get-startable-stoppable";
import { disposer } from "../index";
import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
import { enlistMessageChannelListenerInjectionToken } from "./enlist-message-channel-listener-injection-token";
const listeningOnMessageChannelsInjectable = getInjectable({
id: "listening-on-message-channels",
instantiate: (di) => {
const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken);
const messageChannelListeners = di.injectMany(messageChannelListenerInjectionToken);
return getStartableStoppable("listening-on-channels", () => (
disposer(messageChannelListeners.map(enlistMessageChannelListener))
));
},
});
export default listeningOnMessageChannelsInjectable;
@@ -0,0 +1,51 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainerForInjection } from "@ogre-tools/injectable";
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
export interface MessageChannel<Message> {
id: string;
_messageSignature?: Message; // only used to mark `Message` as used
}
export type MessageChannelHandler<Channel> = Channel extends MessageChannel<infer Message>
? (message: Message) => void
: never;
export interface MessageChannelListener<Channel> {
channel: Channel;
handler: MessageChannelHandler<Channel>;
}
export const messageChannelListenerInjectionToken = getInjectionToken<MessageChannelListener<MessageChannel<unknown>>>(
{
id: "message-channel-listener",
},
);
export interface GetMessageChannelListenerInfo<
Channel extends MessageChannel<Message>,
Message,
> {
id: string;
channel: Channel;
handler: (di: DiContainerForInjection) => MessageChannelHandler<Channel>;
causesSideEffects?: boolean;
}
export function getMessageChannelListenerInjectable<
Channel extends MessageChannel<Message>,
Message,
>(info: GetMessageChannelListenerInfo<Channel, Message>) {
return getInjectable({
id: `${info.channel.id}-listener-${info.id}`,
instantiate: (di) => ({
channel: info.channel,
handler: info.handler(di),
}),
injectionToken: messageChannelListenerInjectionToken,
causesSideEffects: info.causesSideEffects,
});
}
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { MessageChannel } from "./message-channel-listener-injection-token";
export interface SendMessageToChannel {
(channel: MessageChannel<void>): void;
<Message>(channel: MessageChannel<Message>, message: Message): void;
}
export type MessageChannelSender<Channel> = Channel extends MessageChannel<void | undefined>
? () => void
: Channel extends MessageChannel<infer Message>
? (message: Message) => void
: never;
export const sendMessageToChannelInjectionToken = getInjectionToken<SendMessageToChannel>({
id: "send-message-to-message-channel",
});
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface RequestChannel<Request, Response> {
id: string;
_requestSignature?: Request; // used only to mark `Request` as "used"
_responseSignature?: Response; // used only to mark `Response` as "used"
}
@@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { RequestChannel } from "./request-channel-listener-injection-token";
export interface RequestFromChannel {
<Request, Response>(channel: RequestChannel<Request, Response>, request: Request): Promise<Response>;
<Response>(channel: RequestChannel<void, Response>): Promise<Response>;
}
export const requestFromChannelInjectionToken = getInjectionToken<RequestFromChannel>({
id: "request-from-request-channel",
});
@@ -0,0 +1,27 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../cluster-types";
/**
* Grab the `ClusterId` out of a host that was generated by `getClusterFrameUrl`, or nothing
* @param host The host section of a URL
* @returns The `ClusterId` part of the host, or `undefined`
*/
export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345"
const subDomains = host.split(":")[0].split(".");
return subDomains.slice(-3, -2)[0]; // ClusterId or undefined
}
/**
* Get the OpenLens backend routing host for a given `ClusterId`
* @param clusterId The ID to put in front of the current host
* @returns a new URL host section
*/
export function getClusterFrameUrl(clusterId: ClusterId) {
return `//${clusterId}.${location.host}`;
}
@@ -0,0 +1,133 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { runInAction } from "mobx";
import { inspect } from "util";
import { isDefined } from "./type-narrowing";
/**
* Get the value behind `key`. If it was not present, first insert `value`
* @param map The map to interact with
* @param key The key to insert into the map with
* @param value The value to optional add to the map
* @returns The value in the map
*/
export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V {
if (!map.has(key)) {
map.set(key, value);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return map.get(key)!;
}
/**
* Updates map and returns the value that was just inserted
*/
export function put<K, V>(map: Map<K, V>, key: K, value: V): V {
map.set(key, value);
return value;
}
/**
* Like `getOrInsert` but specifically for when `V` is `Map<MK, MV>` so that
* the typings are inferred correctly.
*/
export function getOrInsertMap<K, MK, MV>(map: Map<K, Map<MK, MV>>, key: K): Map<MK, MV> {
return getOrInsert(map, key, new Map<MK, MV>());
}
/**
* Like `getOrInsert` but specifically for when `V` is `Set<any>` so that
* the typings are inferred.
*/
export function getOrInsertSet<K, SK>(map: Map<K, Set<SK>>, key: K): Set<SK> {
return getOrInsert(map, key, new Set<SK>());
}
/**
* A currying version of {@link getOrInsertSet}
*/
export function getOrInsertSetFor<K, SK>(map: Map<K, Set<SK>>): (key: K) => Set<SK> {
return (key) => getOrInsertSet(map, key);
}
/**
* Like `getOrInsert` but with delayed creation of the item. Which is useful
* if it is very expensive to create the initial value.
*/
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, builder: () => V): V;
export function getOrInsertWith<K extends object, V>(map: Map<K, V> | WeakMap<K, V>, key: K, builder: () => V): V;
export function getOrInsertWith<K extends object, V>(map: Map<K, V> | WeakMap<K, V>, key: K, builder: () => V): V {
if (!map.has(key)) {
map.set(key, builder());
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return map.get(key)!;
}
/**
* Like {@link getOrInsertWith} but the builder is async and will be awaited before inserting into the map
*/
export async function getOrInsertWithAsync<K, V>(map: Map<K, V>, key: K, asyncBuilder: () => Promise<V>): Promise<V> {
if (!map.has(key)) {
map.set(key, await asyncBuilder());
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return map.get(key)!;
}
/**
* Set the value associated with `key` iff there was not a previous value
* @param map The map to interact with
* @throws if `key` already in map
* @returns `this` so that `strictSet` can be chained
*/
export function strictSet<K, V>(map: Map<K, V>, key: K, val: V): typeof map {
if (map.has(key)) {
throw new TypeError(`Map already contains key: ${inspect(key)}`);
}
return map.set(key, val);
}
/**
* Get the value associated with `key`
* @param map The map to interact with
* @throws if `key` did not a value associated with it
*/
export function strictGet<K, V>(map: Map<K, V>, key: K): V {
if (!map.has(key)) {
throw new TypeError(`Map does not contains key: ${inspect(key)}`);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return map.get(key)!;
}
/**
* If `key` is in `set`, remove it otherwise add it.
* @param set The set to manipulate
* @param key The key to toggle the "is in"-ness of
*/
export function toggle<K>(set: Set<K>, key: K): void {
runInAction(() => {
// Returns true if value was already in Set; otherwise false.
if (!set.delete(key)) {
set.add(key);
}
});
}
/**
* A helper function to also check for defined-ness
*/
export function includes<T>(src: T[], value: T | null | undefined): boolean {
return isDefined(value) && src.includes(value);
}
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions
export interface Discriminable<T extends string> { readonly kind: T }
// Note: this will fail at transpilation time, if all kinds are not instructed in switch/case.
// See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking
export const checkThatAllDiscriminablesAreExhausted = <T extends never>(value: T) => {
const _exhaustiveCheck: never = value;
return new Error(
`Tried to exhaust discriminables, but no instructions were found for ${(_exhaustiveCheck as any).kind}`,
);
};
@@ -0,0 +1,7 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface Labelable {
readonly label: string;
}
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { sortBy } from "lodash/fp";
export interface Orderable {
readonly orderNumber: number;
}
export type MaybeOrderable = Orderable | object;
export const orderByOrderNumber = <T extends MaybeOrderable>(maybeOrderables: T[]) =>
sortBy(
(orderable) =>
"orderNumber" in orderable
? orderable.orderNumber
: Number.MAX_SAFE_INTEGER,
maybeOrderables,
);
export const byOrderNumber = <T extends Orderable>(left: T, right: T) => (
left.orderNumber - right.orderNumber
);
@@ -0,0 +1,29 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue } from "mobx";
import { isBoolean } from "../../type-narrowing";
export interface Showable {
readonly isShown: IComputedValue<boolean> | boolean;
}
export type MaybeShowable = Showable | object;
export const isShown = (showable: MaybeShowable) => {
if (!("isShown" in showable)) {
return true;
}
if (showable.isShown === undefined) {
return true;
}
if (isBoolean(showable.isShown)) {
return showable.isShown;
}
return showable.isShown.get();
};
@@ -0,0 +1,61 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite/get-composite";
import { compositeHasDescendant } from "./composite-has-descendant";
import { getCompositeFor } from "../get-composite/get-composite";
describe("composite-has-descendant, given composite with children and grand children", () => {
let composite: Composite<{ id: string; parentId?: string }>;
beforeEach(() => {
const items = [
{ id: "some-root-id", parentId: undefined },
{ id: "some-child-item", parentId: "some-root-id" },
{
id: "some-grand-child-item",
parentId: "some-child-item",
},
];
const getComposite = getCompositeFor<{
id: string;
parentId?: string;
}>({
rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
composite = getComposite(items);
});
it("has a child as descendant", () => {
const actual = compositeHasDescendant<typeof composite["value"]>(
(referenceComposite) => referenceComposite.value.id === "some-child-item",
)(composite);
expect(actual).toBe(true);
});
it("has a grand child as descendant", () => {
const actual = compositeHasDescendant<typeof composite["value"]>(
(referenceComposite) =>
referenceComposite.value.id === "some-grand-child-item",
)(composite);
expect(actual).toBe(true);
});
it("does not have an unrelated descendant", () => {
const actual = compositeHasDescendant<typeof composite["value"]>(
(referenceComposite) =>
referenceComposite.value.id === "some-unknown-item",
)(composite);
expect(actual).toBe(false);
});
});
@@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite/get-composite";
const compositeHasDescendant = <T>(
predicate: (referenceComposite: Composite<T>) => boolean,
) => {
const _compositeHasDescendant = (composite: Composite<T>): boolean =>
predicate(composite) ||
!!composite.children.find((childComposite) =>
_compositeHasDescendant(childComposite),
);
return _compositeHasDescendant;
};
export { compositeHasDescendant };
@@ -0,0 +1,91 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite/get-composite";
import { findComposite } from "./find-composite";
import { getCompositeFor } from "../get-composite/get-composite";
describe("find-composite", () => {
let composite: Composite<{ id: string; parentId?: string }>;
beforeEach(() => {
const getComposite = getCompositeFor<{
id: string;
parentId?: string;
}>({
rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
composite = getComposite([
{ id: "some-root-id" },
{ id: "some-child-id", parentId: "some-root-id" },
{ id: "some-grandchild-id", parentId: "some-child-id" },
{ id: "some-other-grandchild-id", parentId: "some-child-id" },
]);
});
it("when finding root using path, does so", () => {
const actual = findComposite("some-root-id")(composite);
expect(actual.id).toBe("some-root-id");
});
it("when finding child using path, does so", () => {
const actual = findComposite("some-root-id", "some-child-id")(composite);
expect(actual.id).toBe("some-child-id");
});
it("when finding grandchild using path, does so", () => {
const actual = findComposite(
"some-root-id",
"some-child-id",
"some-grandchild-id",
)(composite);
expect(actual.id).toBe("some-grandchild-id");
});
it("when finding with non existing leaf-level path, throws", () => {
expect(() => {
findComposite(
"some-root-id",
"some-child-id",
"some-non-existing-grandchild-id",
)(composite);
}).toThrow(`Tried to find 'some-root-id -> some-child-id -> some-non-existing-grandchild-id' from a composite, but found nothing.
Node 'some-root-id -> some-child-id' had only following children:
some-grandchild-id
some-other-grandchild-id`);
});
it("when finding with non-existing mid-level path, throws", () => {
expect(() => {
findComposite(
"some-root-id",
"some-non-existing-child-id",
"some-non-existing-grandchild-id",
)(composite);
}).toThrow(`Tried to find 'some-root-id -> some-non-existing-child-id -> some-non-existing-grandchild-id' from a composite, but found nothing.
Node 'some-root-id' had only following children:
some-child-id`);
});
it("when finding with non-existing root-level path, throws", () => {
expect(() => {
findComposite(
"some-non-existing-root-id",
"some-non-existing-child-id",
"some-non-existing-grandchild-id",
)(composite);
}).toThrow(`Tried to find 'some-non-existing-root-id -> some-non-existing-child-id -> some-non-existing-grandchild-id' from a composite, but found nothing.
Node 'some-root-id' had only following children:
some-child-id`);
});
});
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite/get-composite";
const _findComposite = <T>(currentLeftIds: string[], currentId: string, currentRightIds: string[], composite: Composite<T>): Composite<T> => {
const [nextId, ...nextRightIds] = currentRightIds;
const nextLeftIds = [...currentLeftIds, currentId];
if (currentRightIds.length === 0 && composite.id === currentId) {
return composite;
}
const foundChildComposite = composite.children.find((child) => child.id === nextId);
if (foundChildComposite) {
return _findComposite(nextLeftIds, nextId, nextRightIds, foundChildComposite);
}
const fullPathString = [...currentLeftIds, currentId, ...currentRightIds].join(" -> ");
throw new Error(`Tried to find '${fullPathString}' from a composite, but found nothing.
Node '${[...currentLeftIds, composite.id].join(" -> ")}' had only following children:
${composite.children.map((child) => child.id).join("\n")}`);
};
export const findComposite =
(...path: string[]) =>
<T>(composite: Composite<T>): Composite<T> => {
const [currentId, ...rightIds] = path;
const leftIds: string[] = [];
return _findComposite(leftIds, currentId, rightIds, composite);
};
@@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getCompositeNormalization } from "./get-composite-normalization";
import { getCompositeFor } from "../get-composite/get-composite";
describe("get-composite-normalization", () => {
it("given a composite, flattens it to paths and composites", () => {
const someRootItem = {
id: "some-root-id",
parentId: undefined,
};
const someItem = {
id: "some-id",
parentId: "some-root-id",
};
const someNestedItem = {
id: "some-child-id",
parentId: "some-id",
};
const items = [someRootItem, someItem, someNestedItem];
const getComposite = getCompositeFor<{
id: string;
parentId?: string;
orderNumber?: number;
}>({
rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
const composite = getComposite(items);
const actual = getCompositeNormalization(composite);
expect(actual).toEqual([
[["some-root-id"], expect.objectContaining({ value: someRootItem })],
[["some-root-id", "some-id"], expect.objectContaining({ value: someItem })],
[
["some-root-id", "some-id", "some-child-id"],
expect.objectContaining({ value: someNestedItem }),
],
]);
});
});
@@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite/get-composite";
export const getCompositeNormalization = <T>(composite: Composite<T>) => {
const _normalizeComposite = <T>(
composite: Composite<T>,
previousPath: string[] = [],
): (readonly [path: string[], composite: Composite<T>])[] => {
const currentPath = [...previousPath, composite.id];
const pathAndCompositeTuple = [currentPath, composite] as const;
return [
pathAndCompositeTuple,
...composite.children.flatMap((child) =>
_normalizeComposite(child, currentPath),
),
];
};
return _normalizeComposite(composite);
};
@@ -0,0 +1,71 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getCompositePaths } from "./get-composite-paths";
import { sortBy } from "lodash/fp";
import { getCompositeFor } from "../get-composite/get-composite";
describe("get-composite-paths", () => {
it("given composite with transformed children, returns paths of transformed children in hierarchical order", () => {
const someRootItem = {
id: "some-root-id",
};
const someChildItem1 = {
id: "some-child-id-1",
parentId: "some-root-id",
orderNumber: 1,
};
const someChildItem2 = {
id: "some-child-id-2",
parentId: "some-root-id",
orderNumber: 2,
};
const someGrandchildItem1 = {
id: "some-grandchild-id-1",
parentId: "some-child-id-1",
orderNumber: 1,
};
const someGrandchildItem2 = {
id: "some-grandchild-id-2",
parentId: "some-child-id-1",
orderNumber: 2,
};
const items = [
someRootItem,
// Note: not in order yet.
someChildItem2,
someChildItem1,
someGrandchildItem2,
someGrandchildItem1,
];
const getComposite = getCompositeFor<{
id: string;
parentId?: string;
orderNumber?: number;
}>({
rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
transformChildren: children => sortBy(child => child.orderNumber, children),
});
const composite = getComposite(items);
const actual = getCompositePaths(composite);
expect(actual).toEqual([
["some-root-id"],
["some-root-id", "some-child-id-1"],
["some-root-id", "some-child-id-1", "some-grandchild-id-1"],
["some-root-id", "some-child-id-1", "some-grandchild-id-2"],
["some-root-id", "some-child-id-2"],
]);
});
});
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import { map } from "lodash/fp";
import type { Composite } from "../get-composite/get-composite";
import { getCompositeNormalization } from "../get-composite-normalization/get-composite-normalization";
export const getCompositePaths = (
composite: Composite<unknown>,
): string[][] => pipeline(composite, getCompositeNormalization, map(([path]) => path));
@@ -0,0 +1,363 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "./get-composite";
import { getCompositePaths } from "../get-composite-paths/get-composite-paths";
import { sortBy } from "lodash/fp";
import { getCompositeFor } from "./get-composite";
interface SomeItem {
id: string;
parentId?: string;
orderNumber?: number;
}
describe("get-composite", () => {
it("given items and an explicit root id, creates a composite", () => {
const someRootItem = {
id: "some-root-id",
someProperty: "some-root-content",
};
const someIrrelevantRootItem = {
id: "some-irrelevant-root-id",
someProperty: "some-other-root-content",
};
const someItem = {
id: "some-id",
parentId: "some-root-id",
someProperty: "some-content",
};
const someNestedItem = {
id: "some-nested-id",
parentId: "some-id",
someProperty: "some-nested-content",
};
const items = [someRootItem, someIrrelevantRootItem, someItem, someNestedItem];
const getComposite = getCompositeFor<SomeItem>({
rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
const composite = getComposite(items);
expect(composite).toEqual({
id: "some-root-id",
value: someRootItem,
children: [
{
id: "some-id",
parentId: "some-root-id",
value: someItem,
children: [
{
id: "some-nested-id",
parentId: "some-id",
value: someNestedItem,
children: [],
},
],
},
],
});
});
it("given items and implicit root, creates a composite", () => {
const someRootItem = {
id: "some-root-id",
someProperty: "some-root-content",
// Notice: no "parentId" makes this the implicit root.
parentId: undefined,
};
const someItem = {
id: "some-id",
parentId: "some-root-id",
someProperty: "some-content",
};
const someNestedItem = {
id: "some-nested-id",
parentId: "some-id",
someProperty: "some-nested-content",
};
const items = [someRootItem, someItem, someNestedItem];
const getComposite = getCompositeFor<SomeItem>({
// Notice: no root id
// rootId: "some-root-id",
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
const composite = getComposite(items);
expect(composite).toEqual({
id: "some-root-id",
value: someRootItem,
children: [
{
id: "some-id",
parentId: "some-root-id",
value: someItem,
children: [
{
id: "some-nested-id",
parentId: "some-id",
value: someNestedItem,
children: [],
},
],
},
],
});
});
it("given items and an unspecified root id and multiple items without parent as root, throws", () => {
const someRootItem = {
id: "some-root-id",
// Notice: no "parentId" makes this a root.
parentId: undefined,
};
const someOtherRootItem = {
id: "some-other-root-id",
// Notice: no "parentId" makes also this a root.
parentId: undefined,
};
const items = [someRootItem, someOtherRootItem];
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
expect(() => {
getComposite(items);
}).toThrow(
'Tried to get a composite, but multiple roots where encountered: "some-root-id", "some-other-root-id"',
);
});
it("given non-unique ids, throws", () => {
const someItem = {
id: "some-id",
parentId: "irrelevant",
};
const someOtherItem = {
id: "some-id",
parentId: "irrelevant",
};
const items = [someItem, someOtherItem];
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
expect(() => {
getComposite(items);
}).toThrow(
'Tried to get a composite but encountered non-unique ids: "some-id"',
);
});
it("given items with missing parent ids, when creating composite without handling for unknown parents, throws", () => {
const someItem = {
id: "some-id",
parentId: undefined,
};
const someItemWithMissingParentId = {
id: "some-other-id",
parentId: "some-missing-id",
};
const items = [someItem, someItemWithMissingParentId];
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
expect(() => {
getComposite(items);
}).toThrow(
`Tried to get a composite but encountered missing parent ids: "some-missing-id".
Available parent ids are:
"some-id",
"some-other-id"`,
);
});
describe("given items with missing parents, when creating composite with handling for missing parents", () => {
let composite: Composite<any>;
let handleMissingParentIdMock: jest.Mock;
beforeEach(() => {
const someItem = {
id: "some-root-id",
};
const someItemWithMissingParentId = {
id: "some-orphan-id",
// Note: the item corresponding to this id does not exist,
// making this item have a "missing parent".
parentId: "some-missing-id",
};
const items = [someItem, someItemWithMissingParentId];
handleMissingParentIdMock = jest.fn();
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
handleMissingParentIds: handleMissingParentIdMock,
});
composite = getComposite(items);
});
it("creates composite without the orphan item, and without throwing", () => {
const paths = getCompositePaths(composite);
expect(paths).toEqual([["some-root-id"]]);
});
it("handles the missing parent ids", () => {
expect(handleMissingParentIdMock).toHaveBeenCalledWith({
missingParentIds: ["some-missing-id"],
availableParentIds: ["some-root-id", "some-orphan-id"],
});
});
});
it("given items with same id and parent id, throws", () => {
const someItem = {
id: "some-id",
parentId: "some-id",
};
const someRoot = {
id: "root",
parentId: undefined,
};
const items = [someItem, someRoot];
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
expect(() => {
getComposite(items);
}).toThrow(
'Tried to get a composite, but found items with self as parent: "some-id"',
);
});
it("given undefined ids, throws", () => {
const root = {
parentId: undefined,
id: "some-root",
};
const someItem = {
parentId: "some-root",
id: undefined,
};
const someOtherItem = {
parentId: "some-root",
id: undefined,
};
const items = [root, someItem, someOtherItem];
const getComposite = getCompositeFor<any>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
});
expect(() => {
getComposite(items);
}).toThrow("Tried to get a composite but encountered 2 undefined ids");
});
it("given transformed children, creates a composite with transformed children", () => {
const someRootItem = {
id: "some-root-id",
orderNumber: 1,
};
const someItem1 = {
id: "some-id-1",
parentId: "some-root-id",
orderNumber: 1,
};
const someItem2 = {
id: "some-id-2",
parentId: "some-root-id",
orderNumber: 2,
};
const someChildItem1 = {
id: "some-child-id-1",
parentId: "some-id-1",
orderNumber: 1,
};
const someChildItem2 = {
id: "some-child-id-2",
parentId: "some-id-1",
orderNumber: 2,
};
const items = [
someRootItem,
// Note: not in order yet.
someItem2,
someItem1,
someChildItem2,
someChildItem1,
];
const getComposite = getCompositeFor<SomeItem>({
getId: (x) => x.id,
getParentId: (x) => x.parentId,
transformChildren: (things) =>
sortBy((thing) => thing.orderNumber, things),
});
const composite = getComposite(items);
const orderedPaths = getCompositePaths(composite);
expect(orderedPaths).toEqual([
["some-root-id"],
["some-root-id", "some-id-1"],
["some-root-id", "some-id-1", "some-child-id-1"],
["some-root-id", "some-id-1", "some-child-id-2"],
["some-root-id", "some-id-2"],
]);
});
});
@@ -0,0 +1,147 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import {
countBy,
filter,
toPairs,
nth,
map,
uniq,
without,
compact,
identity,
} from "lodash/fp";
export interface Composite<T> {
id: string;
parentId: string | undefined;
value: T;
children: Composite<T>[];
}
interface Configuration<T> {
rootId?: string;
getId: (thing: T) => string;
getParentId: (thing: T) => string | undefined;
transformChildren?: (things: T[]) => T[];
handleMissingParentIds?: (parentIdsForHandling: ParentIdsForHandling) => void;
}
export const getCompositeFor = <T>({
rootId = undefined,
getId,
getParentId,
transformChildren = identity,
handleMissingParentIds = throwMissingParentIds,
}: Configuration<T>) => (source: T[]) => {
const undefinedIds = pipeline(
source,
filter((x) => getId(x) === undefined),
);
if (undefinedIds.length) {
throw new Error(
`Tried to get a composite but encountered ${undefinedIds.length} undefined ids`,
);
}
const selfReferencingIds = pipeline(
source,
filter((x) => getId(x) === getParentId(x)),
map(getId),
);
if (selfReferencingIds.length) {
throw new Error(
`Tried to get a composite, but found items with self as parent: "${selfReferencingIds.join(
'", ',
)}"`,
);
}
const duplicateIds = pipeline(
source,
countBy(getId),
toPairs,
filter(([, count]) => count > 1),
map(nth(0)),
);
if (duplicateIds.length) {
throw new Error(
`Tried to get a composite but encountered non-unique ids: "${duplicateIds
.map((x) => String(x))
.join('", "')}"`,
);
}
const allIds = pipeline(source, map(getId));
const allParentIds = pipeline(source, map(getParentId), uniq, compact);
const missingParentIds = without(allIds, allParentIds);
if (missingParentIds.length) {
handleMissingParentIds({ missingParentIds, availableParentIds: allIds });
}
const toComposite = (thing: T): Composite<T> => {
const thingId = getId(thing);
return {
id: thingId,
parentId: getParentId(thing),
value: thing,
children: pipeline(
source,
filter((childThing) => {
const parentId = getParentId(childThing);
return parentId === thingId;
}),
transformChildren,
map(toComposite),
),
};
};
const isRootId = rootId
? (thing: T) => getId(thing) === rootId
: (thing: T) => getParentId(thing) === undefined;
const roots = source.filter(isRootId);
if (roots.length > 1) {
throw new Error(
`Tried to get a composite, but multiple roots where encountered: "${roots
.map(getId)
.join('", "')}"`,
);
}
return toComposite(roots[0]);
};
interface ParentIdsForHandling {
missingParentIds: string[];
availableParentIds: string[];
}
const throwMissingParentIds = ({
missingParentIds,
availableParentIds,
}: ParentIdsForHandling) => {
throw new Error(
`Tried to get a composite but encountered missing parent ids: "${missingParentIds.join(
'", "',
)}".\n\nAvailable parent ids are:\n"${availableParentIds.join('",\n"')}"`,
);
};
@@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface ParentOfChildComposite<Id extends string = string> {
id: Id;
}
export interface ChildOfParentComposite<ParentId extends string = string> {
parentId: ParentId;
}
export type RootComposite<Id extends string = string> =
& { parentId: undefined }
& ParentOfChildComposite<Id>;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IComputedValue } from "mobx";
import { computed } from "mobx";
export const computedOr = (...values: IComputedValue<boolean>[]) => computed((
() => values.some(value => value.get())
));
@@ -0,0 +1,40 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { TypedRegEx } from "typed-regex";
// Helper to convert CPU K8S units to numbers
const unitConverters = new Map([
["n", 1000 ** -3],
["u", 1000 ** -2],
["m", 1000 ** -1], // milli
["", 1000 ** 0], // no units
["k", 1000 ** 1],
["M", 1000 ** 2],
["G", 1000 ** 3],
["P", 1000 ** 4],
["T", 1000 ** 5],
["E", 1000 ** 6],
]);
const cpuUnitsRegex = TypedRegEx("^(?<digits>[+-]?[0-9.]+(e[-+]?[0-9]+)?)(?<unit>[EinumkKMGTP]*)$");
export function cpuUnitsToNumber(value: string) {
const match = cpuUnitsRegex.captures(value);
if (!match) {
return undefined;
}
const { digits = "", unit } = match;
const conversion = unitConverters.get(unit);
if (conversion === undefined) {
return undefined;
}
return parseFloat(digits) * conversion;
}
@@ -0,0 +1,63 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import assert from "assert";
import * as iter from "./iter";
// Helper to convert memory from units Ki, Mi, Gi, Ti, Pi to bytes and vise versa
const baseMagnitude = 1024;
const maxMagnitude = ["PiB", baseMagnitude ** 5] as const;
const magnitudes = new Map([
["B", 1] as const,
["KiB", baseMagnitude ** 1] as const,
["MiB", baseMagnitude ** 2] as const,
["GiB", baseMagnitude ** 3] as const,
["TiB", baseMagnitude ** 4] as const,
maxMagnitude,
]);
const unitRegex = /(?<value>[0-9]+(\.[0-9]*)?)(?<suffix>(B|[KMGTP]iB?))?/;
type BinaryUnit = typeof magnitudes extends Map<infer Key, any> ? Key : never;
export function unitsToBytes(value: string): number {
const unitsMatch = value.match(unitRegex);
if (!unitsMatch?.groups) {
return NaN;
}
const parsedValue = parseFloat(unitsMatch.groups.value);
if (!unitsMatch.groups?.suffix) {
return parsedValue;
}
const magnitude = magnitudes.get(unitsMatch.groups.suffix as BinaryUnit)
?? magnitudes.get(`${unitsMatch.groups.suffix}B` as BinaryUnit);
assert(magnitude, "UnitRegex is wrong some how");
return parseInt((parsedValue * magnitude).toFixed(1));
}
export interface BytesToUnitesOptions {
/**
* The number of decimal places. MUST be an integer. MUST be in the range [0, 20].
* @default 1
*/
precision?: number;
}
export function bytesToUnits(bytes: number, { precision = 1 }: BytesToUnitesOptions = {}): string {
if (bytes <= 0 || isNaN(bytes) || !isFinite(bytes)) {
return "N/A";
}
const index = Math.floor(Math.log(bytes) / Math.log(baseMagnitude));
const [suffix, magnitude] = iter.nth(magnitudes.entries(), index) ?? maxMagnitude;
return `${(bytes / magnitude).toFixed(precision)}${suffix}`;
}
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import moment from "moment";
export const getCurrentDateTime = () => moment().utc().format();
export const getMillisecondsFromUnixEpoch = () => Date.now();
export const getSecondsFromUnixEpoch = () => Math.floor(getMillisecondsFromUnixEpoch() / 1000);
+15
View File
@@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Debouncing promise evaluation
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
let timer: NodeJS.Timeout;
return (...params: F) => new Promise(resolve => {
clearTimeout(timer);
timer = global.setTimeout(() => resolve(func(...params)), timeout);
});
}
+24
View File
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type AbortController from "abort-controller";
/**
* Return a promise that will be resolved after at least `timeout` ms have
* passed. If `failFast` is provided then the promise is also resolved if it has
* been aborted.
* @param timeout The number of milliseconds before resolving
* @param failFast An abort controller instance to cause the delay to short-circuit
*/
export function delay(timeout = 1000, failFast?: AbortController): Promise<void> {
return new Promise(resolve => {
const timeoutId = setTimeout(resolve, timeout);
failFast?.signal.addEventListener("abort", () => {
clearTimeout(timeoutId);
resolve();
});
});
}
@@ -0,0 +1,39 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { SingleOrMany } from "./types";
export interface Disposer {
(): void;
}
export interface Disposable {
dispose(): void;
}
export interface ExtendableDisposer extends Disposer {
push(...vals: (Disposer | ExtendableDisposer | Disposable)[]): void;
}
export function disposer(...items: SingleOrMany<Disposer | Disposable | undefined | null>[]): ExtendableDisposer {
return Object.assign(() => {
for (const item of items.flat()) {
if (!item) {
continue;
}
if (typeof item === "function") {
item();
} else {
item.dispose();
}
}
items.length = 0;
}, {
push: (...newItems) => items.push(...newItems),
} as Pick<ExtendableDisposer, "push">);
}
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Helper to sanitize / escape special chars for passing to RegExp-constructor
export function escapeRegExp(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
@@ -0,0 +1,34 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { findExactlyOne } from "./find-exactly-one";
describe("find-exactly-one", () => {
it("when predicate matches to single item, returns the item", () => {
const actual = findExactlyOne((item) => item === "some-item")([
"some-item",
"some-other-item",
]);
expect(actual).toBe("some-item");
});
it("when predicate matches to many items, throws", () => {
expect(() => {
findExactlyOne((item) => item === "some-item")([
"some-item",
"some-item",
]);
}).toThrow("Tried to find exactly one, but found many");
});
it("when predicate does not match, throws", () => {
expect(() => {
findExactlyOne((item) => item === "some-item")([
"some-other-item",
]);
}).toThrow("Tried to find exactly one, but didn't find any");
});
});
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const findExactlyOne = <T>(predicate: (item: T) => boolean) => (collection: T[]): T => {
const itemsFound = collection.filter(predicate);
if (!itemsFound.length) {
throw new Error(
"Tried to find exactly one, but didn't find any",
);
}
if (itemsFound.length > 1) {
throw new Error(
"Tried to find exactly one, but found many",
);
}
return itemsFound[0];
};
@@ -0,0 +1,91 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import moment from "moment";
/**
* This function formats durations in a more human readable form.
* @param timeValue the duration in milliseconds to format
*/
export function formatDuration(timeValue: number, compact = true) {
const duration = moment.duration(timeValue, "milliseconds");
const seconds = Math.floor(duration.asSeconds());
const separator = compact ? "": " ";
if (seconds < 0) {
return "0s";
} else if (seconds < 60*2 ) {
return `${seconds}s`;
}
const minutes = Math.floor(duration.asMinutes());
if (minutes < 10) {
const seconds = duration.seconds();
return getMeaningfulValues([minutes, seconds], ["m", "s"], separator);
} else if (minutes < 60 * 3) {
if (!compact) {
return getMeaningfulValues([minutes, duration.seconds()], ["m", "s"]);
}
return `${minutes}m`;
}
const hours = Math.floor(duration.asHours());
if(hours < 8) {
const minutes = duration.minutes();
return getMeaningfulValues([hours, minutes], ["h", "m"], separator);
} else if (hours < 48) {
if (compact) {
return `${hours}h`;
} else {
return getMeaningfulValues([hours, duration.minutes()], ["h", "m"]);
}
}
const days = Math.floor(duration.asDays());
if (days < 8) {
const hours = duration.hours();
if (compact) {
return getMeaningfulValues([days, hours], ["d", "h"], separator);
} else {
return getMeaningfulValues([days, hours, duration.minutes()], ["d", "h", "m"]);
}
}
const years = Math.floor(duration.asYears());
if (years < 2) {
if (compact) {
return `${days}d`;
} else {
return getMeaningfulValues([days, duration.hours(), duration.minutes()], ["d", "h", "m"]);
}
} else if (years < 8) {
const days = duration.days();
if (compact) {
return getMeaningfulValues([years, days], ["y", "d"], separator);
}
}
if (compact) {
return `${years}y`;
}
return getMeaningfulValues([years, duration.days(), duration.hours(), duration.minutes()], ["y", "d", "h", "m"]);
}
function getMeaningfulValues(values: number[], suffixes: string[], separator = " ") {
return values
.map((a, i): [number, string] => [a, suffixes[i]])
.filter(([dur]) => dur > 0)
.map(([dur, suf]) => dur + suf)
.join(separator);
}
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { createHash } from "crypto";
export function generateNewIdFor(cluster: { kubeConfigPath: string; contextName: string }): string {
return createHash("md5").update(`${cluster.kubeConfigPath}:${cluster.contextName}`).digest("hex");
}
@@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const getErrorMessage = (error: unknown): string => {
if (typeof error === "string") {
return error;
}
if (error instanceof Error) {
return error.message;
}
return JSON.stringify(error);
};
@@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import getRandomIdInjectable from "./get-random-id.injectable";
export default getGlobalOverride(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
@@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { v4 as getRandomId } from "uuid";
const getRandomIdInjectable = getInjectable({
id: "get-random-id",
instantiate: () => () => getRandomId(),
causesSideEffects: true,
});
export default getRandomIdInjectable;
@@ -0,0 +1,62 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { StartableStoppable } from "./get-startable-stoppable";
import { getStartableStoppable } from "./get-startable-stoppable";
describe("getStartableStoppable", () => {
let stopMock: jest.MockedFunction<() => void>;
let startMock: jest.MockedFunction<() => () => void>;
let actual: StartableStoppable;
beforeEach(() => {
stopMock = jest.fn();
startMock = jest.fn().mockImplementation(() => stopMock);
actual = getStartableStoppable("some-id", startMock);
});
it("does not start yet", () => {
expect(startMock).not.toHaveBeenCalled();
});
it("does not stop yet", () => {
expect(stopMock).not.toHaveBeenCalled();
});
it("when stopping before ever starting, throws", () => {
expect(() => actual.stop()).toThrow("Tried to stop \"some-id\", but it is already stopped.");
});
it("is not started", () => {
expect(actual.started).toBe(false);
});
describe("when started", () => {
beforeEach(() => {
actual.start();
});
it("calls start function", () => {
expect(startMock).toHaveBeenCalled();
});
it("is started", () => {
expect(actual.started).toBe(true);
});
describe("when stopped", () => {
beforeEach(() => {
actual.stop();
});
it("calls stop function", () => {
expect(stopMock).toBeCalled();
});
it("is stopped", () => {
expect(actual.started).toBe(false);
});
});
});
});
@@ -0,0 +1,45 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type Stopper = () => void;
export type Starter = () => Stopper;
export interface StartableStoppable {
readonly started: boolean;
start: () => void;
stop: () => void;
}
type StartableStoppableState = "stopped" | "started" | "starting";
export function getStartableStoppable(id: string, startAndGetStopper: Starter): StartableStoppable {
let stop: Stopper;
let state: StartableStoppableState = "stopped";
return {
get started() {
return state === "started";
},
start: () => {
if (state !== "stopped") {
throw new Error(`Tried to start "${id}", but it is already ${state}.`);
}
state = "starting";
stop = startAndGetStopper();
state = "started";
},
stop: () => {
if (state !== "started") {
throw new Error(`Tried to stop "${id}", but it is already ${state}.`);
}
stop();
state = "stopped";
},
};
}
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// Create random system name
export function getRandId({ prefix = "", suffix = "", sep = "_" } = {}) {
const randId = () => Math.random().toString(16).slice(2);
return [prefix, randId(), suffix].filter(s => s).join(sep);
}
+245
View File
@@ -0,0 +1,245 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IInterceptable, IInterceptor, IListenable, ISetWillChange, ObservableMap } from "mobx";
import { action, observable, ObservableSet } from "mobx";
export function makeIterableIterator<T>(iterator: Iterator<T>): IterableIterator<T> {
(iterator as IterableIterator<T>)[Symbol.iterator] = () => iterator as IterableIterator<T>;
return iterator as IterableIterator<T>;
}
export class HashSet<T> implements Set<T> {
#hashmap: Map<string, T>;
constructor(initialValues: Iterable<T>, protected hasher: (item: T) => string) {
this.#hashmap = new Map<string, T>(Array.from(initialValues, value => [this.hasher(value), value]));
}
replace(other: ObservableHashSet<T> | ObservableSet<T> | Set<T> | readonly T[]): this {
if (other === null || other === undefined) {
return this;
}
if (!(Array.isArray(other) || other instanceof Set || other instanceof ObservableHashSet || other instanceof ObservableSet)) {
throw new Error(`ObservableHashSet: Cannot initialize set from ${other}`);
}
this.clear();
for (const value of other) {
this.add(value);
}
return this;
}
clear(): void {
this.#hashmap.clear();
}
add(value: T): this {
this.#hashmap.set(this.hasher(value), value);
return this;
}
toggle(value: T): void {
const hash = this.hasher(value);
if (this.#hashmap.has(hash)) {
this.#hashmap.delete(hash);
} else {
this.#hashmap.set(hash, value);
}
}
delete(value: T): boolean {
return this.#hashmap.delete(this.hasher(value));
}
forEach(callbackfn: (value: T, key: T, set: Set<T>) => void, thisArg?: any): void {
this.#hashmap.forEach(value => callbackfn(value, value, thisArg ?? this));
}
has(value: T): boolean {
return this.#hashmap.has(this.hasher(value));
}
get size(): number {
return this.#hashmap.size;
}
entries(): IterableIterator<[T, T]> {
let nextIndex = 0;
const keys = Array.from(this.keys());
const values = Array.from(this.values());
return makeIterableIterator<[T, T]>({
next() {
const index = nextIndex++;
return index < values.length
? { value: [keys[index], values[index]], done: false }
: { done: true, value: undefined };
},
});
}
keys(): IterableIterator<T> {
return this.values();
}
values(): IterableIterator<T> {
let nextIndex = 0;
const observableValues = Array.from(this.#hashmap.values());
return makeIterableIterator<T>({
next: () => {
return nextIndex < observableValues.length
? { value: observableValues[nextIndex++], done: false }
: { done: true, value: undefined };
},
});
}
[Symbol.iterator](): IterableIterator<T> {
return this.#hashmap.values();
}
get [Symbol.toStringTag](): string {
return "Set";
}
toJSON(): T[] {
return Array.from(this);
}
toString(): string {
return "[object Set]";
}
}
export class ObservableHashSet<T> implements Set<T>, IInterceptable<ISetWillChange>, IListenable {
#hashmap: ObservableMap<string, T>;
get interceptors_(): IInterceptor<ISetWillChange<T>>[] {
return [];
}
get changeListeners_(): Function[] {
return [];
}
constructor(initialValues: Iterable<T>, protected hasher: (item: T) => string) {
this.#hashmap = observable.map<string, T>(Array.from(initialValues, value => [this.hasher(value), value]), undefined);
}
@action
replace(other: ObservableHashSet<T> | ObservableSet<T> | Set<T> | readonly T[]): this {
if (other === null || other === undefined) {
return this;
}
if (!(Array.isArray(other) || other instanceof Set || other instanceof ObservableHashSet || other instanceof ObservableSet)) {
throw new Error(`ObservableHashSet: Cannot initialize set from ${other}`);
}
this.clear();
for (const value of other) {
this.add(value);
}
return this;
}
clear(): void {
this.#hashmap.clear();
}
add(value: T): this {
this.#hashmap.set(this.hasher(value), value);
return this;
}
@action
toggle(value: T): void {
const hash = this.hasher(value);
if (this.#hashmap.has(hash)) {
this.#hashmap.delete(hash);
} else {
this.#hashmap.set(hash, value);
}
}
delete(value: T): boolean {
return this.#hashmap.delete(this.hasher(value));
}
forEach(callbackfn: (value: T, key: T, set: Set<T>) => void, thisArg?: any): void {
this.#hashmap.forEach(value => callbackfn(value, value, thisArg ?? this));
}
has(value: T): boolean {
return this.#hashmap.has(this.hasher(value));
}
get size(): number {
return this.#hashmap.size;
}
entries(): IterableIterator<[T, T]> {
let nextIndex = 0;
const keys = Array.from(this.keys());
const values = Array.from(this.values());
return makeIterableIterator<[T, T]>({
next() {
const index = nextIndex++;
return index < values.length
? { value: [keys[index], values[index]], done: false }
: { done: true, value: undefined };
},
});
}
keys(): IterableIterator<T> {
return this.values();
}
values(): IterableIterator<T> {
let nextIndex = 0;
const observableValues = Array.from(this.#hashmap.values());
return makeIterableIterator<T>({
next: () => {
return nextIndex < observableValues.length
? { value: observableValues[nextIndex++], done: false }
: { done: true, value: undefined };
},
});
}
[Symbol.iterator](): IterableIterator<T> {
return this.#hashmap.values();
}
get [Symbol.toStringTag](): string {
return "Set";
}
toJSON(): T[] {
return Array.from(this);
}
toString(): string {
return "[object ObservableSet]";
}
}
+52
View File
@@ -0,0 +1,52 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./abort-controller";
export * from "./autobind";
export * from "./camelCase";
export * from "./cluster-id-url-parsing";
export * from "./collection-functions";
export * from "./convertCpu";
export * from "./convertMemory";
export * from "./debouncePromise";
export * from "./delay";
export * from "./disposer";
export * from "./escapeRegExp";
export * from "./formatDuration";
export * from "./getRandId";
export * from "./hash-set";
export * from "./n-fircate";
export * from "./noop";
export * from "./observable-crate/impl";
export * from "./promise-exec";
export * from "./readonly";
export * from "./reject-promise";
export * from "./singleton";
export * from "./sort-compare";
export * from "./splitArray";
export * from "./tar";
export * from "./toJS";
export * from "./type-narrowing";
export * from "./types";
export * from "./wait-for-path";
export * from "./wait";
export type { Tuple } from "./tuple";
import * as iter from "./iter";
import * as array from "./array";
import * as tuple from "./tuple";
import * as base64 from "./base64";
import * as object from "./objects";
import * as json from "./json";
export {
iter,
array,
tuple,
base64,
object,
json,
};
@@ -0,0 +1,31 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isPromise } from "./is-promise";
describe("isPromise", () => {
it("given promise, returns true", () => {
const actual = isPromise(new Promise(() => {}));
expect(actual).toBe(true);
});
it("given non-promise, returns false", () => {
const actual = isPromise({});
expect(actual).toBe(false);
});
it("given thenable, returns false", () => {
const actual = isPromise({ then: () => {} });
expect(actual).toBe(false);
});
it("given nothing, returns false", () => {
const actual = isPromise(undefined);
expect(actual).toBe(false);
});
});
@@ -0,0 +1,7 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export function isPromise(reference: any): reference is Promise<any> {
return reference?.constructor === Promise;
}
+248
View File
@@ -0,0 +1,248 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type Falsey = false | 0 | "" | null | undefined;
interface Iterator<T> extends Iterable<T> {
filter(fn: (val: T) => unknown): Iterator<T>;
filterMap<U>(fn: (val: T) => Falsey | U): Iterator<U>;
find(fn: (val: T) => unknown): T | undefined;
collect<U>(fn: (values: Iterable<T>) => U): U;
map<U>(fn: (val: T) => U): Iterator<U>;
flatMap<U>(fn: (val: T) => U[]): Iterator<U>;
concat(src2: IterableIterator<T>): Iterator<T>;
join(sep?: string): string;
}
export function chain<T>(src: IterableIterator<T>): Iterator<T> {
return {
filter: (fn) => chain(filter(src, fn)),
filterMap: (fn) => chain(filterMap(src, fn)),
map: (fn) => chain(map(src, fn)),
flatMap: (fn) => chain(flatMap(src, fn)),
find: (fn) => find(src, fn),
join: (sep) => join(src, sep),
collect: (fn) => fn(src),
concat: (src2) => chain(concat(src, src2)),
[Symbol.iterator]: () => src,
};
}
/**
* Create a new type safe empty Iterable
* @returns An `Iterable` that yields 0 items
*/
export function* newEmpty<T>(): IterableIterator<T> {}
/**
* Creates a new `Iterable` that yields at most n items from src.
* Does not modify `src` which can be used later.
* @param src An initial iterator
* @param n The maximum number of elements to take from src. Yields up to the floor of `n` and 0 items if n < 0
*/
export function* take<T>(src: Iterable<T>, n: number): IterableIterator<T> {
outer: for (let i = 0; i < n; i += 1) {
for (const item of src) {
yield item;
continue outer;
}
// if we are here that means that `src` has been exhausted. Don't bother trying again.
break outer;
}
}
/**
* Creates a new iterator that iterates (lazily) over its input and yields the
* result of `fn` for each item.
* @param src A type that can be iterated over
* @param fn The function that is called for each value
*/
export function* map<T, U>(src: Iterable<T>, fn: (from: T) => U): IterableIterator<U> {
for (const from of src) {
yield fn(from);
}
}
/**
* The single layer flattening of an iterator, discarding `Falsey` values.
* @param src A type that can be iterated over
* @param fn The function that returns either an iterable over items that should be filtered out or a `Falsey` value indicating that it should be ignored
*/
export function* filterFlatMap<T, U>(src: Iterable<T>, fn: (from: T) => Iterable<U | Falsey> | Falsey): IterableIterator<U> {
for (const from of src) {
if (!from) {
continue;
}
const mapping = fn(from);
if (!mapping) {
continue;
}
for (const mapped of mapping) {
if (mapped) {
yield mapped;
}
}
}
}
/**
* Returns a new iterator that yields the items that each call to `fn` would produce
* @param src A type that can be iterated over
* @param fn A function that returns an iterator
*/
export function* flatMap<T, U>(src: Iterable<T>, fn: (from: T) => Iterable<U>): IterableIterator<U> {
for (const from of src) {
yield* fn(from);
}
}
/**
* Creates a new iterator that iterates (lazily) over its input and yields the
* items that return a `truthy` value from `fn`.
* @param src A type that can be iterated over
* @param fn The function that is called for each value
*/
export function* filter<T>(src: Iterable<T>, fn: (from: T) => any): IterableIterator<T> {
for (const from of src) {
if (fn(from)) {
yield from;
}
}
}
/**
* Creates a new iterator that iterates (lazily) over its input and yields the
* result of `fn` when it is `truthy`
* @param src A type that can be iterated over
* @param fn The function that is called for each value
*/
export function* filterMap<T, U>(src: Iterable<T>, fn: (from: T) => U | Falsey): IterableIterator<U> {
for (const from of src) {
const res = fn(from);
if (res) {
yield res;
}
}
}
/**
* Creates a new iterator that iterates (lazily) over its input and yields the
* result of `fn` when it is not null or undefined
* @param src A type that can be iterated over
* @param fn The function that is called for each value
*/
export function* filterMapStrict<T, U>(src: Iterable<T>, fn: (from: T) => U | null | undefined): IterableIterator<U> {
for (const from of src) {
const res = fn(from);
if (res != null) {
yield res;
}
}
}
/**
* Iterate through `src` until `match` returns a truthy value
* @param src A type that can be iterated over
* @param match A function that should return a truthy value for the item that you want to find
* @returns The first entry that `match` returns a truthy value for, or `undefined`
*/
export function find<T>(src: Iterable<T>, match: (i: T) => any): T | undefined {
for (const from of src) {
if (match(from)) {
return from;
}
}
return void 0;
}
/**
* Iterate over `src` calling `reducer` with the previous produced value and the current
* yielded value until `src` is exausted. Then return the final value.
* @param src The value to iterate over
* @param reducer A function for producing the next item from an accumilation and the current item
* @param initial The initial value for the iteration
*/
export function reduce<T, R extends Iterable<any>>(src: Iterable<T>, reducer: (acc: R, cur: T) => R, initial: R): R;
export function reduce<T, R = T>(src: Iterable<T>, reducer: (acc: R, cur: T) => R, initial: R): R {
let acc = initial;
for (const item of src) {
acc = reducer(acc, item);
}
return acc;
}
/**
* A convenience function for reducing over an iterator of strings and concatenating them together
* @param src The value to iterate over
* @param connector The string value to intersperse between the yielded values
* @returns The concatenated entries of `src` interspersed with copies of `connector`
*/
export function join(src: IterableIterator<unknown>, connector = ","): string {
const iterSrc = src[Symbol.iterator]();
const first = iterSrc.next();
if (first.done === true) {
return "";
}
return reduce(iterSrc, (acc, cur) => `${acc}${connector}${cur}`, `${first.value}`);
}
/**
* Iterate `n` times and then return the next value.
* @param src The value to iterate over
* @param n The zero-index value for the item to return to.
*/
export function nth<T>(src: Iterable<T>, n: number): T | undefined {
const iterator = src[Symbol.iterator]();
while (n --> 0) {
iterator.next();
}
return iterator.next().value;
}
/**
* A convenience function to get the first item of an iterator
* @param src The value to iterate over
*/
export function first<T>(src: Iterable<T>): T | undefined {
return nth(src, 0);
}
/**
* Iterate through `src` and return `true` if `fn` returns a thruthy value for every yielded value.
* Otherwise, return `false`. This function short circuits.
* @param src The type to be iterated over
* @param fn A function to check each iteration
*/
export function every<T>(src: Iterable<T>, fn: (val: T) => any): boolean {
for (const val of src) {
if (!fn(val)) {
return false;
}
}
return true;
}
export function* concat<T>(...sources: IterableIterator<T>[]): IterableIterator<T> {
for (const source of sources) {
for (const val of source) {
yield val;
}
}
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { JsonValue } from "type-fest";
export function parse(input: string): JsonValue {
return JSON.parse(input);
}
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* Split an iterable into several arrays with matching fields
* @param from The iterable of items to split up
* @param field The field of each item to split over
* @param parts What each array will be filtered to
* @returns A `parts.length` tuple of `T[]` where each array has matching `field` values
*/
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: []): [];
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: [T[typeof field]]): [T[]];
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: [T[typeof field], T[typeof field]]): [T[], T[]];
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: [T[typeof field], T[typeof field], T[typeof field]]): [T[], T[], T[]];
export function nFircate<T>(from: Iterable<T>, field: keyof T, parts: T[typeof field][]): T[][] {
if (new Set(parts).size !== parts.length) {
throw new TypeError("Duplicate parts entries");
}
const res = Array.from(parts, () => [] as T[]);
for (const item of from) {
const index = parts.indexOf(item[field]);
if (index < 0) {
continue;
}
res[index].push(item);
}
return res;
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* A function that does nothing
*/
export function noop<T extends any[]>(...args: T): void {
return void args;
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* A better typed version of `Object.fromEntries` where the keys are known to
* be a specific subset
*/
export function fromEntries<T, Key extends string>(entries: Iterable<readonly [Key, T]>): Record<Key, T> {
return Object.fromEntries(entries) as Record<Key, T>;
}
export function keys<K extends keyof any>(obj: Partial<Record<K, any>>): K[];
export function keys<K extends keyof any>(obj: Record<K, any>): K[] {
return Object.keys(obj) as K[];
}
export function entries<K extends string, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
export function entries<K extends string | number | symbol, V>(obj: Partial<Record<K, V>> | null | undefined): [K, V][];
export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][];
export function entries<K extends string | number | symbol, V>(obj: Record<K, V> | null | undefined): [K, V][] {
if (obj && typeof obj == "object") {
return Object.entries(obj) as never;
}
return [] as never;
}
@@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observable, runInAction } from "mobx";
import { getOrInsertMap } from "../collection-functions";
import { noop } from "../noop";
export interface ObservableCrate<T> {
get(): T;
set(value: T): void;
}
export interface ObservableCrateFactory {
<T>(initialValue: T, transitionHandlers?: ObservableCrateTransitionHandlers<T>): ObservableCrate<T>;
}
export interface ObservableCrateTransitionHandler<T> {
from: T;
to: T;
onTransition: () => void;
}
export type ObservableCrateTransitionHandlers<T> = ObservableCrateTransitionHandler<T>[];
function convertToHandlersMap<T>(handlers: ObservableCrateTransitionHandlers<T>): Map<T, Map<T, () => void>> {
const res: ReturnType<typeof convertToHandlersMap<T>> = new Map();
for (const { from, to, onTransition } of handlers) {
getOrInsertMap(res, from).set(to, onTransition);
}
return res;
}
export const observableCrate = ((initialValue, transitionHandlers = []) => {
const crate = observable.box(initialValue);
const handlers = convertToHandlersMap(transitionHandlers);
return {
get() {
return crate.get();
},
set(value) {
const onTransition = handlers.get(crate.get())?.get(value) ?? noop;
runInAction(() => {
crate.set(value);
onTransition();
});
},
};
}) as ObservableCrateFactory;
@@ -0,0 +1,116 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ObservableCrate } from "./impl";
import { observableCrate } from "./impl";
describe("observable-crate", () => {
it("can be constructed with initial value", () => {
expect(() => observableCrate(0)).not.toThrow();
});
it("has a definite type if the initial value is provided", () => {
expect (() => {
const res: ObservableCrate<number> = observableCrate(0);
void res;
}).not.toThrow();
});
it("accepts an array of transitionHandlers", () => {
expect(() => observableCrate(0, [])).not.toThrow();
});
describe("with a crate over an enum, and some transition handlers", () => {
enum Test {
Start,
T1,
End,
}
let crate: ObservableCrate<Test>;
let correctHandler: jest.MockedFunction<() => void>;
let incorrectHandler: jest.MockedFunction<() => void>;
beforeEach(() => {
correctHandler = jest.fn();
incorrectHandler = jest.fn();
crate = observableCrate(Test.Start, [
{
from: Test.Start,
to: Test.Start,
onTransition: incorrectHandler,
},
{
from: Test.Start,
to: Test.T1,
onTransition: correctHandler,
},
{
from: Test.Start,
to: Test.End,
onTransition: incorrectHandler,
},
{
from: Test.T1,
to: Test.Start,
onTransition: incorrectHandler,
},
{
from: Test.T1,
to: Test.T1,
onTransition: incorrectHandler,
},
{
from: Test.T1,
to: Test.End,
onTransition: incorrectHandler,
},
{
from: Test.End,
to: Test.Start,
onTransition: incorrectHandler,
},
{
from: Test.End,
to: Test.T1,
onTransition: incorrectHandler,
},
{
from: Test.End,
to: Test.End,
onTransition: incorrectHandler,
},
]);
});
it("initial value is available", () => {
expect(crate.get()).toBe(Test.Start);
});
it("does not call any transition handler", () => {
expect(correctHandler).not.toBeCalled();
expect(incorrectHandler).not.toBeCalled();
});
describe("when setting a new value", () => {
beforeEach(() => {
crate.set(Test.T1);
});
it("calls the associated transition handler", () => {
expect(correctHandler).toBeCalled();
});
it("does not call any other transition handler", () => {
expect(incorrectHandler).not.toBeCalled();
});
it("new value is available", () => {
expect(crate.get()).toBe(Test.T1);
});
});
});
});
@@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import openLinkInBrowserInjectable from "./open-link-in-browser.injectable";
export default getGlobalOverride(openLinkInBrowserInjectable, () => async () => {});
@@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shell } from "electron";
const allowedProtocols = new Set(["http:", "https:"]);
export type OpenLinkInBrowser = (url: string) => Promise<void>;
const openLinkInBrowserInjectable = getInjectable({
id: "open-link-in-browser",
instantiate: (): OpenLinkInBrowser => (
async (url) => {
const { protocol } = new URL(url);
if (!allowedProtocols.has(protocol)) {
throw new TypeError("not an http(s) URL");
}
await shell.openExternal(url);
}
),
causesSideEffects: true,
});
export default openLinkInBrowserInjectable;
@@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import * as util from "util";
import { execFile } from "child_process";
export const promiseExecFile = util.promisify(execFile);
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import randomBytesInjectable from "./random-bytes.injectable";
export default getGlobalOverride(randomBytesInjectable, () => async (size) => {
const res = Buffer.alloc(size);
for (let i = 0; i < size; i += 1) {
res[i] = i;
}
return res;
});
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { randomBytes } from "crypto";
import { promisify } from "util";
export type RandomBytes = (size: number) => Promise<Buffer>;
const randomBytesInjectable = getInjectable({
id: "random-bytes",
instantiate: (): RandomBytes => promisify(randomBytes),
causesSideEffects: true,
});
export default randomBytesInjectable;
@@ -0,0 +1,70 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import { render } from "@testing-library/react";
import type { IComputedValue } from "mobx";
import { computed, observe } from "mobx";
import React from "react";
import { observer } from "mobx-react";
import { advanceFakeTime, testUsingFakeTime } from "../../test-utils/use-fake-time";
import { reactiveNow } from "./reactive-now";
describe("reactiveNow", () => {
let someComputed: IComputedValue<boolean>;
beforeEach(() => {
testUsingFakeTime("2015-10-21T07:28:00Z");
someComputed = computed(() => {
const currentTimestamp = reactiveNow();
return currentTimestamp > new Date("2015-10-21T07:28:00Z").getTime();
});
});
describe("react-context", () => {
let rendered: RenderResult;
beforeEach(() => {
const TestComponent = observer(
({ someComputed }: { someComputed: IComputedValue<boolean> }) => (
<div>{someComputed.get() ? "true" : "false"}</div>
),
);
rendered = render(<TestComponent someComputed={someComputed} />);
});
it("given time passes, works", () => {
advanceFakeTime(1000);
expect(rendered.container.textContent).toBe("true");
});
it("does not share the state from previous test", () => {
expect(rendered.container.textContent).toBe("false");
});
});
describe("non-react-context", () => {
let actual: boolean;
beforeEach(() => {
observe(someComputed, (changed) => {
actual = changed.newValue as boolean;
}, true);
});
it("given time passes, works", () => {
advanceFakeTime(1000);
expect(actual).toBe(true);
});
it("does not share the state from previous test", () => {
expect(actual).toBe(false);
});
});
});
@@ -0,0 +1,60 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { _isComputingDerivation } from "mobx";
import type { IResource } from "mobx-utils";
import { fromResource } from "mobx-utils";
// Note: This file is copy-pasted from mobx-utils to fix very specific issue.
// TODO: Remove this file once https://github.com/mobxjs/mobx-utils/issues/306 is fixed.
const tickers: Record<number|string, IResource<number>> = {};
export function reactiveNow(interval?: number | "frame") {
if (interval === void 0) { interval = 1000; }
if (!_isComputingDerivation()) {
// See #40
return Date.now();
}
// Note: This is the kludge until https://github.com/mobxjs/mobx-utils/issues/306 is fixed
const synchronizationIsEnabled = !process.env.JEST_WORKER_ID;
if (!tickers[interval] || !synchronizationIsEnabled) {
if (typeof interval === "number")
tickers[interval] = createIntervalTicker(interval);
else
tickers[interval] = createAnimationFrameTicker();
}
return tickers[interval].current();
}
function createIntervalTicker(interval: number) {
let subscriptionHandle: NodeJS.Timer;
return fromResource(function (sink) {
sink(Date.now());
subscriptionHandle = setInterval(function () { return sink(Date.now()); }, interval);
}, function () {
clearInterval(subscriptionHandle);
}, Date.now());
}
function createAnimationFrameTicker() {
const frameBasedTicker = fromResource(function (sink) {
sink(Date.now());
function scheduleTick() {
window.requestAnimationFrame(function () {
sink(Date.now());
if (frameBasedTicker.isAlive())
scheduleTick();
});
}
scheduleTick();
}, function () { }, Date.now());
return frameBasedTicker;
}
@@ -0,0 +1,93 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { Readable } from "readable-stream";
import type { TypedArray } from "type-fest";
/**
* ReadableWebToNodeStream
*
* Copied from https://github.com/Borewit/readable-web-to-node-stream
*
* Adds read error handler
*
* */
export class ReadableWebToNodeStream<T extends TypedArray> extends Readable {
public bytesRead = 0;
public released = false;
/**
* Default web API stream reader
* https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader
*/
private reader: ReadableStreamDefaultReader<T>;
private pendingRead?: Promise<ReadableStreamReadResult<T>>;
/**
*
* @param stream ReadableStream: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
*/
constructor(stream: ReadableStream<T>) {
super();
this.reader = stream.getReader();
}
/**
* Implementation of readable._read(size).
* When readable._read() is called, if data is available from the resource,
* the implementation should begin pushing that data into the read queue
* https://nodejs.org/api/stream.html#stream_readable_read_size_1
*/
public async _read() {
// Should start pushing data into the queue
// Read data from the underlying Web-API-readable-stream
if (this.released) {
this.push(null); // Signal EOF
return;
}
try {
this.pendingRead = this.reader.read();
const data = await this.pendingRead;
// clear the promise before pushing pushing new data to the queue and allow sequential calls to _read()
delete this.pendingRead;
if (data.done || this.released) {
this.push(null); // Signal EOF
} else {
this.bytesRead += data.value.length;
this.push(data.value); // Push new data to the queue
}
} catch (error) {
this.push(null); // Signal EOF
}
}
/**
* If there is no unresolved read call to Web-API ReadableStream immediately returns;
* otherwise will wait until the read is resolved.
*/
public async waitForReadToComplete() {
if (this.pendingRead) {
await this.pendingRead;
}
}
/**
* Close wrapper
*/
public async close(): Promise<void> {
await this.syncAndRelease();
}
private async syncAndRelease() {
this.released = true;
await this.waitForReadToComplete();
await this.reader.releaseLock();
}
}
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ReadonlyDeep } from "type-fest";
export function readonly<T>(src: T): ReadonlyDeep<T> {
return src as ReadonlyDeep<T>;
}
@@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AbortSignal } from "abort-controller";
/**
* Creates a new promise that will be rejected when the signal rejects.
*
* Useful for `Promise.race()` applications.
* @param signal The AbortController's signal to reject with
*/
export function rejectPromiseBy(signal: AbortSignal): Promise<never> {
return new Promise((_, reject) => {
signal.addEventListener("abort", reject);
});
}
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RequestChannel } from "../channel/request-channel-listener-injection-token";
export type ResolveSystemProxyChannel = RequestChannel<string, string>;
export const resolveSystemProxyChannel: ResolveSystemProxyChannel = {
id: "resolve-system-proxy-channel",
};
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
export type ResolveSystemProxy = (url: string) => Promise<string>;
export const resolveSystemProxyInjectionToken = getInjectionToken<ResolveSystemProxy>({
id: "resolve-system-proxy",
});
@@ -0,0 +1,79 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export interface StaticThis<T, R extends any[]> { new(...args: R): T }
/**
* @deprecated This is a form of global shared state
*/
export class Singleton {
private static readonly instances = new WeakMap<object, Singleton>();
private static creating = "";
constructor() {
if (Singleton.creating.length === 0) {
throw new TypeError("A singleton class must be created by createInstance()");
}
}
/**
* Creates the single instance of the child class if one was not already created.
*
* Multiple calls will return the same instance.
* Essentially throwing away the arguments to the subsequent calls.
*
* Note: this is a racy function, if two (or more) calls are racing to call this function
* only the first's arguments will be used.
* @param this Implicit argument that is the child class type
* @param args The constructor arguments for the child class
* @returns An instance of the child class
*/
static createInstance<T extends Singleton, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
if (!Singleton.instances.has(this)) {
if (Singleton.creating.length > 0) {
throw new TypeError(`Cannot create a second singleton (${this.name}) while creating a first (${Singleton.creating})`);
}
try {
Singleton.creating = this.name;
Singleton.instances.set(this, new this(...args));
} finally {
Singleton.creating = "";
}
}
return Singleton.instances.get(this) as T;
}
/**
* Get the instance of the child class that was previously created.
* @param this Implicit argument that is the child class type
* @param strict If false will return `undefined` instead of throwing when an instance doesn't exist.
* Default: `true`
* @returns An instance of the child class
*/
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict?: true): T;
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict: false): T | undefined;
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict = true): T | undefined {
if (!Singleton.instances.has(this) && strict) {
throw new TypeError(`instance of ${this.name} is not created`);
}
return Singleton.instances.get(this) as (T | undefined);
}
/**
* Delete the instance of the child class.
*
* Note: this doesn't prevent callers of `getInstance` from storing the result in a global.
*
* There is *no* way in JS or TS to prevent globals like that.
*/
static resetInstance() {
Singleton.instances.delete(this);
}
}
export default Singleton;
@@ -0,0 +1,75 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import semver, { coerce } from "semver";
export enum Ordering {
LESS = -1,
EQUAL = 0,
GREATER = 1,
}
/**
* This function switches the direction of `ordering` if `direction` is `"desc"`
* @param ordering The original ordering (assumed to be an "asc" ordering)
* @param direction The new desired direction
*/
export function rectifyOrdering(ordering: Ordering, direction: "asc" | "desc"): Ordering {
if (direction === "desc") {
return -ordering;
}
return ordering;
}
/**
* An ascending sorting function
* @param left An item from an array
* @param right An item from an array
* @returns The relative ordering in an ascending manner.
* - Less if left < right
* - Equal if left == right
* - Greater if left > right
*/
export function sortCompare<T>(left: T, right: T): Ordering {
if (left < right) {
return Ordering.LESS;
}
if (left === right) {
return Ordering.EQUAL;
}
return Ordering.GREATER;
}
/**
* This function sorts of list of items that have what should be a semver version formated string
* as the field `version` but if it is not loosely coercable to semver falls back to sorting them
* alphanumerically
*/
export function sortBySemverVersion<T extends { version: string }>(versioneds: T[]): T[] {
return versioneds
.map(versioned => ({
__version: coerce(versioned.version, { loose: true }),
raw: versioned,
}))
.sort((left, right) => {
if (left.__version && right.__version) {
return semver.compare(right.__version, left.__version);
}
if (!left.__version && right.__version) {
return Ordering.GREATER;
}
if (left.__version && !right.__version) {
return Ordering.LESS;
}
return sortCompare(left.raw.version, right.raw.version);
})
.map(({ raw }) => raw);
}
@@ -0,0 +1,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* Get an ordering function based on the function getter
*/
export function byValue<T>(getOrderValue: (src: T) => number): (left: T, right: T) => number {
return (left, right) => {
const leftValue = getOrderValue(left);
const rightValue = getOrderValue(right);
return leftValue - rightValue;
};
}
@@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* This function splits an array into two sub arrays on the first instance of
* element (from the left). If the array does not contain the element. The
* return value is defined to be `[array, [], false]`. If the element is in
* the array then the return value is `[left, right, true]` where `left` is
* the elements of `array` from `[0, index)` and `right` is `(index, length)`
* @param array the full array to split into two sub-arrays
* @param element the element in the middle of the array
* @returns the left and right sub-arrays which when conjoined with `element`
* is the same as `array`, and `true`
*/
export function splitArray<T>(array: T[], element: T): [T[], T[], boolean] {
const index = array.indexOf(element);
if (index < 0) {
return [array, [], false];
}
return [array.slice(0, index), array.slice(index + 1, array.length), true];
}
/**
* Splits an array into two parts based on the outcome of `condition`. If `true`
* the value will be returned as part of the right array. If `false` then part of
* the left array.
* @param src the full array to bifurcate
* @param condition the function to determine which set each is in
*/
export function bifurcateArray<T>(src: T[], condition: (item: T) => boolean): [falses: T[], trues: T[]] {
const res: [T[], T[]] = [[], []];
for (const item of src) {
res[+condition(item)].push(item);
}
return res;
}
@@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { syncBoxChannel } from "./channels";
import { getMessageChannelListenerInjectable } from "../channel/message-channel-listener-injection-token";
import syncBoxStateInjectable from "./sync-box-state.injectable";
const syncBoxChannelListenerInjectable = getMessageChannelListenerInjectable({
id: "init",
channel: syncBoxChannel,
handler: (di) => ({ id, value }) => di.inject(syncBoxStateInjectable, id).set(value),
});
export default syncBoxChannelListenerInjectable;
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MessageChannel } from "../channel/message-channel-listener-injection-token";
import type { RequestChannel } from "../channel/request-channel-listener-injection-token";
export type SyncBoxChannel = MessageChannel<{ id: string; value: any }>;
export const syncBoxChannel: SyncBoxChannel = {
id: "sync-box-channel",
};
export type SyncBoxInitialValueChannel = RequestChannel<
void,
{ id: string; value: any }[]
>;
export const syncBoxInitialValueChannel: SyncBoxInitialValueChannel = {
id: "sync-box-initial-value-channel",
};
@@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { IObservableValue } from "mobx";
import { computed } from "mobx";
import { syncBoxChannel } from "./channels";
import { sendMessageToChannelInjectionToken } from "../channel/message-to-channel-injection-token";
import syncBoxStateInjectable from "./sync-box-state.injectable";
import type { SyncBox } from "./sync-box-injection-token";
import { toJS } from "../toJS";
const createSyncBoxInjectable = getInjectable({
id: "create-sync-box",
instantiate: (di) => {
const messageToChannel = di.inject(sendMessageToChannelInjectionToken);
const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id);
return <Value>(id: string, initialValue: Value): SyncBox<Value> => {
const state = getSyncBoxState(id) as IObservableValue<Value>;
state.set(initialValue);
return {
id,
value: computed(() => toJS(state.get())),
set: (value) => {
state.set(value);
messageToChannel(syncBoxChannel, { id, value });
},
};
};
},
});
export default createSyncBoxInjectable;
@@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { MessageChannelHandler } from "../channel/message-channel-listener-injection-token";
import type { SyncBoxChannel } from "./channels";
import syncBoxStateInjectable from "./sync-box-state.injectable";
const syncBoxChannelHandlerInjectable = getInjectable({
id: "sync-box-channel-handler",
instantiate: (di): MessageChannelHandler<SyncBoxChannel> => {
const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id);
return ({ id, value }) => getSyncBoxState(id)?.set(value);
},
});
export default syncBoxChannelHandlerInjectable;
@@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
export interface SyncBox<Value> {
id: string;
value: IComputedValue<Value>;
set: (value: Value) => void;
}
export const syncBoxInjectionToken = getInjectionToken<SyncBox<any>>({
id: "sync-box",
});
@@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { observable } from "mobx";
const syncBoxStateInjectable = getInjectable({
id: "sync-box-state",
instantiate: () => observable.box(),
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, id: string) => id,
}),
});
export default syncBoxStateInjectable;
@@ -0,0 +1,172 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { observe, runInAction } from "mobx";
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import createSyncBoxInjectable from "./create-sync-box.injectable";
import type { SyncBox } from "./sync-box-injection-token";
import { syncBoxInjectionToken } from "./sync-box-injection-token";
describe("sync-box", () => {
let applicationBuilder: ApplicationBuilder;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
applicationBuilder.beforeApplicationStart(mainDi => {
runInAction(() => {
mainDi.register(someInjectable);
});
});
applicationBuilder.beforeWindowStart((windowDi) => {
runInAction(() => {
windowDi.register(someInjectable);
});
});
});
describe("given application is started, when value is set in main", () => {
let valueInMain: string;
let syncBoxInMain: SyncBox<string>;
beforeEach(async () => {
await applicationBuilder.startHidden();
syncBoxInMain = applicationBuilder.mainDi.inject(someInjectable);
observe(syncBoxInMain.value, ({ newValue }) => {
valueInMain = newValue as string;
}, true);
runInAction(() => {
syncBoxInMain.set("some-value-from-main");
});
});
it("knows value in main", () => {
expect(valueInMain).toBe("some-value-from-main");
});
describe("when window starts", () => {
let valueInRenderer: string;
let syncBoxInRenderer: SyncBox<string>;
let rendererDi: DiContainer;
beforeEach(async () => {
const applicationWindow =
applicationBuilder.applicationWindow.create("some-window-id");
await applicationWindow.start();
rendererDi = applicationWindow.di;
syncBoxInRenderer = rendererDi.inject(someInjectable);
observe(syncBoxInRenderer.value, ({ newValue }) => {
valueInRenderer = newValue as string;
}, true);
});
it("has the value from main", () => {
expect(valueInRenderer).toBe("some-value-from-main");
});
describe("when value is set from renderer", () => {
beforeEach(() => {
runInAction(() => {
syncBoxInRenderer.set("some-value-from-renderer");
});
});
it("has value in main", () => {
expect(valueInMain).toBe("some-value-from-renderer");
});
it("has value in renderer", () => {
expect(valueInRenderer).toBe("some-value-from-renderer");
});
});
});
});
describe("when application starts with a window", () => {
let valueInRenderer: string;
let valueInMain: string;
let syncBoxInMain: SyncBox<string>;
let syncBoxInRenderer: SyncBox<string>;
beforeEach(async () => {
await applicationBuilder.render();
const applicationWindow = applicationBuilder.applicationWindow.only;
syncBoxInMain = applicationBuilder.mainDi.inject(someInjectable);
syncBoxInRenderer = applicationWindow.di.inject(someInjectable);
observe(syncBoxInRenderer.value, ({ newValue }) => {
valueInRenderer = newValue as string;
}, true);
observe(syncBoxInMain.value, ({ newValue }) => {
valueInMain = newValue as string;
}, true);
});
it("knows initial value in main", () => {
expect(valueInMain).toBe("some-initial-value");
});
it("knows initial value in renderer", () => {
expect(valueInRenderer).toBe("some-initial-value");
});
describe("when value is set from main", () => {
beforeEach(() => {
runInAction(() => {
syncBoxInMain.set("some-value-from-main");
});
});
it("has value in main", () => {
expect(valueInMain).toBe("some-value-from-main");
});
it("has value in renderer", () => {
expect(valueInRenderer).toBe("some-value-from-main");
});
describe("when value is set from renderer", () => {
beforeEach(() => {
runInAction(() => {
syncBoxInRenderer.set("some-value-from-renderer");
});
});
it("has value in main", () => {
expect(valueInMain).toBe("some-value-from-renderer");
});
it("has value in renderer", () => {
expect(valueInRenderer).toBe("some-value-from-renderer");
});
});
});
});
});
const someInjectable = getInjectable({
id: "some-injectable",
instantiate: (di) => {
const createSyncBox = di.inject(createSyncBoxInjectable);
return createSyncBox("some-sync-box", "some-initial-value");
},
injectionToken: syncBoxInjectionToken,
});
Loaded 100 of 3321 files, more files were not shown because too many files have changed in this diff. Show more