@conjecture-dev/g-std
Version:
A collection of TypeScript utility functions for common programming tasks
785 lines • 28.5 kB
JavaScript
"use strict";
import assert from 'node:assert';
import { deepObjectFilterK, ice, objectMap, arrayFilterMap, all, enumerate, findDuplicates, f, objectFromEntries, objectEntries, canonicalize, throuncedAsync, Failure, Success, ChangeDetector } from '.';
import { describe, test } from 'node:test';
void describe("test that ice freezes objects", () => {
void test("test it freezes objects", () => {
const obj = { a: 1, b: 2 };
const frozen = ice(obj);
assert(Object.isFrozen(frozen));
try {
frozen.a = 2;
assert.fail("Expected an error to be thrown");
}
catch (error) {
assert.strictEqual(error.message, "Cannot assign to read only property 'a' of object '#<Object>'");
}
});
void test("test it freezes nested objects", () => {
const obj = { a: 1, b: ice({ c: 2 }) };
const frozen = ice(obj);
assert(Object.isFrozen(frozen.b));
try {
frozen.b.c = 3;
assert.fail("Expected an error to be thrown");
}
catch (error) {
assert.strictEqual(error.message, "Cannot assign to read only property 'c' of object '#<Object>'");
}
});
void test("test it freezes arrays", () => {
const arr = [1, 2, 3];
const frozen = ice(arr);
assert(Object.isFrozen(frozen));
try {
frozen[0] = 2;
assert.fail("Expected an error to be thrown");
}
catch (error) {
assert.strictEqual(error.message, "Cannot assign to read only property '0' of object '[object Array]'");
}
});
void test("test you can't add more properties", () => {
const obj = { a: 1, b: 2 };
const frozen = ice(obj);
try {
// @ts-ignore
frozen.c = 3;
assert.fail("Expected an error to be thrown");
}
catch (error) {
assert.strictEqual(error.message, "Cannot add property c, object is not extensible");
}
});
});
void describe("filters and maps on objects", () => {
void describe("deepObjectFilterK", () => {
const hiddenFilter = (key) => !(typeof key === 'string' && key.startsWith('__'));
void test("shallow", () => {
const obj = { a: 1, b: 2, c: 3, d: 4, __private: 5 };
const filtered = deepObjectFilterK(obj, hiddenFilter);
assert.deepStrictEqual(filtered, { a: 1, b: 2, c: 3, d: 4 });
});
void test("deep", () => {
const obj = { a: 1, __wee: 'lol', b: { c: 2, d: { e: 3, __private: 4 } } };
const filtered = deepObjectFilterK(obj, hiddenFilter);
assert.deepStrictEqual(filtered, { a: 1, b: { c: 2, d: { e: 3 } } });
});
void test("deep with array", () => {
const obj = { a: 1, __wee: 'lol', b: [2, 3, { e: 3, __private: 4 }] };
const filtered = deepObjectFilterK(obj, hiddenFilter);
console.log(filtered);
assert.deepStrictEqual(filtered, { a: 1, b: [2, 3, { e: 3 }] });
});
});
});
void describe("test that objectMap works", () => {
void test("testing that it maps over objects", () => {
const obj = { a: 1, b: 2, c: 3 };
const mapped = objectMap(obj, (key, value) => ({ key, value: value * 2 }));
assert.deepStrictEqual(mapped, { a: { key: 'a', value: 2 }, b: { key: 'b', value: 4 }, c: { key: 'c', value: 6 } });
});
void test("provides correct index parameter", () => {
const obj = { a: 1, b: 2, c: 3 };
const mapped = objectMap(obj, (_, __, index) => index);
assert.deepStrictEqual(mapped, { a: 0, b: 1, c: 2 });
});
void test("handles empty objects", () => {
const obj = {};
const mapped = objectMap(obj, (_, value) => value);
assert.deepStrictEqual(mapped, {});
});
void test("preserves property types", () => {
const obj = { str: "hello", num: 42, bool: true };
const mapped = objectMap(obj, (_, value) => ({ original: value }));
assert.deepStrictEqual(mapped, {
str: { original: "hello" },
num: { original: 42 },
bool: { original: true }
});
});
void test("can transform value types", () => {
const obj = { a: 1, b: 2, c: 3 };
const mapped = objectMap(obj, (_, value) => String(value));
assert.deepStrictEqual(mapped, { a: "1", b: "2", c: "3" });
});
});
void describe("test arrayFilterMap", () => {
void test("filters out none values", () => {
const arr = [1, 2, 3, 4];
const result = arrayFilterMap(arr, x => x % 2 === 0 ? ['some', x * 2] : ['none']);
assert.deepStrictEqual(result, [4, 8]);
});
void test("handles empty array", () => {
const arr = [];
const result = arrayFilterMap(arr, x => ['some', x * 2]);
assert.deepStrictEqual(result, []);
});
void test("can filter out everything", () => {
const arr = [1, 2, 3];
const result = arrayFilterMap(arr, () => ['none']);
assert.deepStrictEqual(result, []);
});
void test("can keep everything with transformation", () => {
const arr = ['a', 'b', 'c'];
const result = arrayFilterMap(arr, x => ['some', x.toUpperCase()]);
assert.deepStrictEqual(result, ['A', 'B', 'C']);
});
void test("can transform types", () => {
const arr = ['1', '2', 'not a number', '3'];
const result = arrayFilterMap(arr, x => {
const num = Number(x);
return isNaN(num) ? ['none'] : ['some', num];
});
assert.deepStrictEqual(result, [1, 2, 3]);
});
});
void describe("test deepObjectFilterK", () => {
void test("filters shallow object", () => {
const obj = { keep: 1, remove: 2, also_keep: 3 };
const result = deepObjectFilterK(obj, key => !key.toString().includes('remove'));
assert.deepStrictEqual(result, { keep: 1, also_keep: 3 });
});
void test("filters nested objects", () => {
const obj = {
keep: { remove: 1, keep: 2 },
remove: { keep: 3 },
also_keep: { nested: { remove: 4, keep: 5 } }
};
const result = deepObjectFilterK(obj, key => !key.toString().includes('remove'));
assert.deepStrictEqual(result, {
keep: { keep: 2 },
also_keep: { nested: { keep: 5 } }
});
});
void test("handles arrays", () => {
const obj = {
keep: [
{ remove: 1, keep: 2 },
{ keep: 3, remove: 4 }
],
remove: [1, 2, 3]
};
const result = deepObjectFilterK(obj, key => !key.toString().includes('remove'));
assert.deepStrictEqual(result, {
keep: [
{ keep: 2 },
{ keep: 3 }
]
});
});
void test("handles primitive values", () => {
const obj = {
keep: 1,
remove: "string",
nested: {
keep: true,
remove: null
}
};
const result = deepObjectFilterK(obj, key => !key.toString().includes('remove'));
assert.deepStrictEqual(result, {
keep: 1,
nested: {
keep: true
}
});
});
void test("handles null values", () => {
const obj = {
keep: null,
remove: null,
nested: null
};
const result = deepObjectFilterK(obj, key => !key.toString().includes('remove'));
assert.deepStrictEqual(result, {
keep: null,
nested: null
});
});
void test("handles empty objects and arrays", () => {
const obj = {
keep: {},
remove: [],
nested: {
keep: [],
remove: {}
}
};
const result = deepObjectFilterK(obj, key => !key.toString().includes('remove'));
assert.deepStrictEqual(result, {
keep: {},
nested: {
keep: []
}
});
});
});
void describe("test all", () => {
void test("returns true when all elements are truthy", () => {
assert.strictEqual(all([1, 2, 3, true, "hello", {}]), true);
});
void test("returns false if any element is falsy", () => {
assert.strictEqual(all([1, 2, 0, 4]), false);
assert.strictEqual(all([1, "", 3]), false);
assert.strictEqual(all([1, false, 3]), false);
assert.strictEqual(all([1, null, 3]), false);
assert.strictEqual(all([1, undefined, 3]), false);
});
void test("returns true for empty iterable", () => {
assert.strictEqual(all([]), true);
});
void test("works with Set", () => {
assert.strictEqual(all(new Set([1, 2, 3])), true);
assert.strictEqual(all(new Set([1, 0, 3])), false);
});
void test("works with custom iterables", () => {
const customIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
}
};
assert.strictEqual(all(customIterable), true);
const customIterableWithFalsy = {
*[Symbol.iterator]() {
yield 1;
yield 0;
yield 3;
}
};
assert.strictEqual(all(customIterableWithFalsy), false);
});
});
void describe("test enumerate", () => {
void test("returns an array of tuples", () => {
const result = enumerate([1, 2, 3]);
assert.deepStrictEqual(result, [[0, 1], [1, 2], [2, 3]]);
});
void test("works with empty array", () => {
const result = enumerate([]);
assert.deepStrictEqual(result, []);
});
});
void describe("test canonicalize", () => {
void test("handles primitive values", () => {
assert.strictEqual(canonicalize(42), "42");
assert.strictEqual(canonicalize("hello"), "\"hello\"");
assert.strictEqual(canonicalize(true), "true");
assert.strictEqual(canonicalize(null), "null");
});
void test("sorts object keys", () => {
const obj = { c: 1, a: 2, b: 3 };
const expected = `{
"a": 2,
"b": 3,
"c": 1
}`;
assert.strictEqual(canonicalize(obj), expected);
});
void test("handles nested objects", () => {
const obj = {
b: { z: 1, y: 2, x: 3 },
a: { c: 4, b: 5, a: 6 }
};
const expected = `{
"a": {
"a": 6,
"b": 5,
"c": 4
},
"b": {
"x": 3,
"y": 2,
"z": 1
}
}`;
assert.strictEqual(canonicalize(obj), expected);
});
void test("handles arrays", () => {
const arr = [3, 1, 2];
const expected = `[
3,
1,
2
]`;
assert.strictEqual(canonicalize(arr), expected);
});
void test("handles arrays of objects", () => {
const arr = [
{ b: 1, a: 2 },
{ d: 3, c: 4 }
];
const expected = `[
{
"a": 2,
"b": 1
},
{
"c": 4,
"d": 3
}
]`;
assert.strictEqual(canonicalize(arr), expected);
});
void test("handles objects with array values", () => {
const obj = {
b: [3, 1, 2],
a: [6, 4, 5]
};
const expected = `{
"a": [
6,
4,
5
],
"b": [
3,
1,
2
]
}`;
assert.strictEqual(canonicalize(obj), expected);
});
void test("preserves array order except for nested objects", () => {
const arr = [
[3, 1, 2],
{ b: 1, a: 2 },
[6, 4, 5]
];
const expected = `[
[
3,
1,
2
],
{
"a": 2,
"b": 1
},
[
6,
4,
5
]
]`;
assert.strictEqual(canonicalize(arr), expected);
});
void test("handles empty objects and arrays", () => {
assert.strictEqual(canonicalize({}), "{}");
assert.strictEqual(canonicalize([]), "[]");
assert.strictEqual(canonicalize({ a: [], b: {} }), `{
"a": [],
"b": {}
}`);
});
});
void describe("test findDuplicates", () => {
void test("finds duplicate primitives", () => {
const arr = [1, 2, 2, 3, 3, 3, 4];
assert.deepStrictEqual(findDuplicates(arr), [2, 3, 3]);
});
void test("finds duplicate objects regardless of key order", () => {
const arr = [
{ a: 1, b: 2 },
{ b: 2, a: 1 }, // Same as first object
{ a: 1, b: 3 }
];
assert.deepStrictEqual(findDuplicates(arr), [{ b: 2, a: 1 }]);
});
void test("handles empty array", () => {
assert.deepStrictEqual(findDuplicates([]), []);
});
void test("handles array with no duplicates", () => {
const arr = [1, 2, 3, 4];
assert.deepStrictEqual(findDuplicates(arr), []);
});
void test("finds duplicate nested structures", () => {
const arr = [
{ a: { x: 1 }, b: [1, 2] },
{ a: { x: 2 }, b: [1, 2] },
{ a: { x: 1 }, b: [1, 2] } // Same as first object
];
assert.deepStrictEqual(findDuplicates(arr), [
{ a: { x: 1 }, b: [1, 2] }
]);
});
void test("handles arrays as elements", () => {
const arr = [
[1, 2],
[2, 3],
[1, 2], // Duplicate of first array
[2, 3] // Duplicate of second array
];
assert.deepStrictEqual(findDuplicates(arr), [
[1, 2],
[2, 3]
]);
});
void test("handles mixed types", () => {
const arr = [
1,
"1",
{ value: 1 },
{ value: 1 }, // Duplicate object
1, // Duplicate number
"2"
];
assert.deepStrictEqual(findDuplicates(arr), [
1,
{ value: 1 }
]);
});
});
void describe("test f", () => {
void test("basic string interpolation", () => {
const name = "Alice";
const result = f `Hello ${name}!`;
assert.strictEqual(result, "Hello Alice!");
});
void test("multiple interpolations", () => {
const name = "Bob";
const age = 25;
const result = f `${name} is ${age} years old`;
assert.strictEqual(result, "Bob is 25 years old");
});
void test("handles boolean values", () => {
const isActive = true;
const result = f `The status is ${isActive}`;
assert.strictEqual(result, "The status is true");
});
void test("handles number values", () => {
const count = 42;
const result = f `Count: ${count}`;
assert.strictEqual(result, "Count: 42");
});
void test("handles empty strings", () => {
const empty = "";
const result = f `Start${empty}End`;
assert.strictEqual(result, "StartEnd");
});
void test("handles multiple empty strings", () => {
const result = f `${""} ${""} ${""}`;
assert.strictEqual(result, " ");
});
void test("throws on invalid types", () => {
const obj = { toString: () => "invalid" };
assert.throws(
// @ts-expect-error - Testing runtime type checking
() => f `Invalid ${obj}`, {
message: "Was not a string"
});
});
void test("handles consecutive interpolations", () => {
const a = "one";
const b = "two";
const result = f `${a}${b}`;
assert.strictEqual(result, "onetwo");
});
void test("preserves whitespace", () => {
const word = "test";
const result = f ` ${word} `;
assert.strictEqual(result, " test ");
});
void test("handles zero interpolations", () => {
const result = f `plain string`;
assert.strictEqual(result, "plain string");
});
});
void describe("test objectFromEntries", () => {
void test("creates object from string keys", () => {
const entries = [
["a", 1],
["b", 2],
["c", 3]
];
const result = objectFromEntries(entries);
assert.deepStrictEqual(result, { a: 1, b: 2, c: 3 });
});
void test("creates object from number keys", () => {
const entries = [
[1, "one"],
[2, "two"],
[3, "three"]
];
const result = objectFromEntries(entries);
assert.deepStrictEqual(result, { 1: "one", 2: "two", 3: "three" });
});
void test("handles empty array", () => {
const entries = [];
const result = objectFromEntries(entries);
assert.deepStrictEqual(result, {});
});
void test("handles object values", () => {
const entries = [
["person1", { name: "Alice", age: 30 }],
["person2", { name: "Bob", age: 25 }]
];
const result = objectFromEntries(entries);
assert.deepStrictEqual(result, {
person1: { name: "Alice", age: 30 },
person2: { name: "Bob", age: 25 }
});
});
void test("preserves value references", () => {
const obj1 = { id: 1 };
const obj2 = { id: 2 };
const entries = [
["a", obj1],
["b", obj2]
];
const result = objectFromEntries(entries);
assert.deepStrictEqual(result, { a: obj1, b: obj2 });
});
});
void describe("test objectEntries", () => {
void test("gets entries from string keys", () => {
const obj = {
a: 1,
b: 2,
c: 3
};
const result = objectEntries(obj);
assert.deepStrictEqual(result, [
["a", 1],
["b", 2],
["c", 3]
]);
});
void test("gets entries from number keys", () => {
const obj = {
1: "one",
2: "two",
3: "three"
};
const result = objectEntries(obj);
assert.deepStrictEqual(result, [
["1", "one"], // Note: number keys are converted to strings
["2", "two"],
["3", "three"]
]);
});
void test("handles empty object", () => {
const obj = {};
const result = objectEntries(obj);
assert.deepStrictEqual(result, []);
});
void test("handles object values", () => {
const obj = {
person1: { name: "Alice", age: 30 },
person2: { name: "Bob", age: 25 }
};
const result = objectEntries(obj);
assert.deepStrictEqual(result, [
["person1", { name: "Alice", age: 30 }],
["person2", { name: "Bob", age: 25 }]
]);
});
void test("preserves value references", () => {
const value1 = { id: 1 };
const value2 = { id: 2 };
const obj = {
a: value1,
b: value2
};
const result = objectEntries(obj);
assert.strictEqual(result[0][1], value1);
assert.strictEqual(result[1][1], value2);
});
void test("handles mixed value types", () => {
const obj = {
str: "hello",
num: 42
};
const result = objectEntries(obj);
assert.deepStrictEqual(result, [
["str", "hello"],
["num", 42]
]);
});
void test("maintains entry order", () => {
const obj = {
z: 3,
y: 2,
x: 1
};
const result = objectEntries(obj);
assert.deepStrictEqual(result, [
["z", 3],
["y", 2],
["x", 1]
]);
});
});
void describe("test throuncedAsync", async () => {
const FLAKY_FACTOR = 5;
const EXECUTION_TIME_FUDGE = 1;
const throunceMs = 10 * FLAKY_FACTOR;
const aLittleTime = 1 * FLAKY_FACTOR;
void test("no throuncing at the start", async () => {
let executed = [];
const fn = throuncedAsync((x) => {
executed = [...executed, x];
return x;
}, { throunceMs });
const t1 = Date.now();
const result = await fn.fn("woowee");
const t2 = Date.now();
assert.deepStrictEqual(result, Success("woowee"));
assert.deepStrictEqual(t2 - t1 < aLittleTime, true, `This should execute right away, instead took (${t2 - t1} ms)`);
assert.deepStrictEqual(executed, ["woowee"]);
});
void test("throunce right after", async () => {
let executed = [];
const fn = throuncedAsync((x) => {
executed = [...executed, x];
return x;
}, { throunceMs });
const t1 = Date.now();
// fill the bandwidth for the next 50 ms
void fn.fn("woowee");
// this should not be executed right away
const promise = fn.fn("424242");
await new Promise((res) => setTimeout(res, aLittleTime));
assert.deepStrictEqual(executed, ["woowee"]);
await promise;
assert.deepStrictEqual(executed, ["woowee", "424242"]);
const t2 = Date.now();
assert.deepStrictEqual(t2 - t1 + EXECUTION_TIME_FUDGE >= throunceMs, true, `This should be throunced, instead took (${t2 - t1} ms)`);
});
void test("override call during throuncing window", async () => {
let executed = [];
const fn = throuncedAsync((x) => {
executed = [...executed, x];
return x;
}, { throunceMs });
const t1 = Date.now();
// fill the bandwidth for the next 50 ms
void fn.fn("woowee");
// this should not be executed at all
const rejectedPromise = fn.fn("424242");
// wait for less time than the window
await new Promise((res) => setTimeout(res, throunceMs / 2));
assert.deepStrictEqual(executed, ["woowee"], "Second call shouldn't have executed yet");
const promise = fn.fn("azerty");
await promise;
assert.deepStrictEqual(executed, ["woowee", "azerty"], "424242 should have been overriden");
assert.deepStrictEqual(await rejectedPromise, Failure("throttled"), "This should have been rejected");
const t2 = Date.now();
assert.deepStrictEqual(t2 - t1 + EXECUTION_TIME_FUDGE >= throunceMs, true, `This should be throunced, instead took (${t2 - t1} ms)`);
});
void test("execute immediately after throuncing window", async () => {
let executed = [];
const fn = throuncedAsync((x) => {
executed = [...executed, x];
return x;
}, { throunceMs });
const t1 = Date.now();
// fill the bandwidth for the next 50 ms
void fn.fn("woowee");
// this should not be executed at all
const rejectedPromise = fn.fn("424242");
// wait for less time than the window
await new Promise((res) => setTimeout(res, throunceMs / 2));
assert.deepStrictEqual(executed, ["woowee"], "Second call shouldn't have executed yet");
const windowStart = Date.now();
const promise = fn.fn("azerty");
await promise;
assert.deepStrictEqual(executed, ["woowee", "azerty"], "424242 should have been overriden");
assert.deepStrictEqual(await rejectedPromise, Failure("throttled"), "This should have been rejected");
const t2 = Date.now();
assert.deepStrictEqual(t2 - t1 + EXECUTION_TIME_FUDGE >= throunceMs, true, `This should be throunced, instead took (${t2 - t1} ms)`);
// we say that foobar should execute right away, so let's make sure that the window from woowee has actually passed
await new Promise((res) => setTimeout(res, (windowStart + throunceMs) - Date.now()));
const t3 = Date.now();
await fn.fn("foobar");
assert.deepStrictEqual(executed, ["woowee", "azerty", "foobar"], "This should have been executed");
const t4 = Date.now();
assert.deepStrictEqual(t4 - t3 < aLittleTime, true, `This should execute right away, instead took (${t4 - t3} ms)`);
});
void test("twice in a row", async () => {
let executed = [];
const fn = throuncedAsync((x) => {
executed = [...executed, x];
return x;
}, { throunceMs });
{
const t1 = Date.now();
// fill the bandwidth for the next throunceMs
await fn.fn("woowee");
{
const t2 = Date.now();
assert.deepStrictEqual(t2 - t1 < aLittleTime, true, `This should execute right away, instead took (${t2 - t1} ms)`);
assert.deepStrictEqual(executed, ["woowee"]);
}
// this should not be executed at all
const rejectedPromise = fn.fn("424242");
// wait for less time than the window
await new Promise((res) => setTimeout(res, throunceMs / 2));
assert.deepStrictEqual(executed, ["woowee"], "Second call shouldn't have executed yet");
const result = await fn.fn("azerty");
assert.deepStrictEqual(result, Success("azerty"));
assert.deepStrictEqual(executed, ["woowee", "azerty"], "424242 should have been overriden");
assert.deepStrictEqual(await rejectedPromise, Failure("throttled"), "This should have been rejected");
{
const t2 = Date.now();
assert.deepStrictEqual(t2 - t1 + EXECUTION_TIME_FUDGE >= throunceMs, true, `This should be throunced, instead took (${t2 - t1} ms)`);
}
}
executed = [];
// wait for the window that started when "azerty" was sent
await new Promise((res) => setTimeout(res, throunceMs));
{
const t1 = Date.now();
// fill the bandwidth for the next throunceMs
await fn.fn("woowee");
{
const t2 = Date.now();
assert.deepStrictEqual(t2 - t1 < aLittleTime, true, `This should execute right away, instead took (${t2 - t1} ms)`);
assert.deepStrictEqual(executed, ["woowee"]);
}
// this should not be executed at all
const rejectedPromise = fn.fn("424242");
// wait for less time than the window
await new Promise((res) => setTimeout(res, throunceMs / 2));
assert.deepStrictEqual(executed, ["woowee"], "Second call shouldn't have executed yet");
const result = await fn.fn("azerty");
assert.deepStrictEqual(result, Success("azerty"));
assert.deepStrictEqual(executed, ["woowee", "azerty"], "424242 should have been overriden");
assert.deepStrictEqual(await rejectedPromise, Failure("throttled"), "This should have been rejected");
{
const t2 = Date.now();
assert.deepStrictEqual(t2 - t1 + EXECUTION_TIME_FUDGE >= throunceMs, true, `This should be throunced, instead took (${t2 - t1} ms)`);
}
}
});
});
void describe("ChangeDetector", () => {
void test("detects changes correctly", () => {
const detector = new ChangeDetector();
// First value should always be detected as changed
assert.strictEqual(detector.hasChanged({ a: 1, b: "test" }), true);
// Same value should not be detected as changed
assert.strictEqual(detector.hasChanged({ a: 1, b: "test" }), false);
// Different value should be detected as changed
assert.strictEqual(detector.hasChanged({ a: 2, b: "test" }), true);
// Same value again should not be detected as changed
assert.strictEqual(detector.hasChanged({ a: 2, b: "test" }), false);
});
void test("objects with same values in different order are not detected as changed", () => {
const detector = new ChangeDetector();
const obj1 = { a: 3, b: "test" };
const obj2 = { b: "test", a: 3 };
assert.strictEqual(detector.hasChanged(obj1), true);
assert.strictEqual(detector.hasChanged(obj2), false);
});
void test("nested objects work the same way", () => {
const detector = new ChangeDetector();
const nested1 = { a: 1, b: { c: 2, d: "test" } };
const nested2 = { a: 1, b: { c: 2, d: "test" } };
assert.strictEqual(detector.hasChanged(nested1), true);
assert.strictEqual(detector.hasChanged(nested2), false);
});
});
//# sourceMappingURL=index.test.js.map