UNPKG

@conjecture-dev/g-std

Version:

A collection of TypeScript utility functions for common programming tasks

785 lines 28.5 kB
"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