From 7e4f7f8488767971cfdcd9e1161db8d4429d6bf6 Mon Sep 17 00:00:00 2001 From: Curstantine Date: Tue, 4 Mar 2025 21:40:48 +0530 Subject: [PATCH 1/3] feat: preliminary result class --- packages/core/src/result.js | 114 ++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 packages/core/src/result.js diff --git a/packages/core/src/result.js b/packages/core/src/result.js new file mode 100644 index 0000000..a784c4a --- /dev/null +++ b/packages/core/src/result.js @@ -0,0 +1,114 @@ +/** + * @constant {symbol} empty + */ +export const empty = Symbol("empty"); + +/** + * @template T, E + */ +export default class Result { + /** @type {T | typeof empty} */ + value; + + /** @type {E | typeof empty} */ + error; + + /** + * @param {T | typeof empty} value + * @param {E | typeof empty} error + */ + constructor(value, error) { + this.error = error; + this.value = value; + } + + /** + * @returns {this is Result} + */ + isOk() { + return this.value !== empty; + } + + /** + * @returns {this is Result} + */ + isErr() { + return this.error !== empty; + } + + /** + * Unwraps a result, yielding the content of an `Ok`. + * + * @returns {T} + * @throws {E} When called on an `Err` value + */ + unwrap() { + if (this.value === empty) throw this.error; + return this.value; + } + + /** + * Unwraps a result, yielding the content of an `Err`. + * + * @returns {E} + * @throws {Error} When called on an `Ok` value + */ + unwrapErr() { + if (this.error === empty) { + throw new Error("called `Result.unwrapErr()` on an `Ok` value"); + } + + return this.error; + } + + /** + * @template T + * + * @param {T} value + * @returns {Result} + */ + static ok(value) { + return new Result(value, empty); + } + + /** + * @template E + * + * @param {E} error + * @returns {Result} error + */ + static err(error) { + return new Result(empty, error); + } + + /** + * @template T, E + * + * @param {() => T} fn + * @param {(e: unknown) => E} err + * + * @returns {Result} + */ + static run(fn, err) { + try { + return Result.ok(fn()); + } catch (error) { + return Result.err(err(error)); + } + } + + /** + * @template T, E + * + * @param {() => Promise} fn + * @param {null | ((e: unknown) => E)} [err=null] + * @returns {Promise>} + */ + static async runAsync(fn, err = null) { + try { + return Result.ok(await fn.call(null)); + } catch (error) { + return Result.err(err === null ? error : err.call(null, error)); + } + } +} From f473f0b52e4345d80550d62fd42d27f7b47dc5f2 Mon Sep 17 00:00:00 2001 From: Curstantine Date: Fri, 17 Jul 2026 23:12:40 +0530 Subject: [PATCH 2/3] feat(core): add Result type with map, chain, and unwrap methods Export the Result class as a named export, add `map`, `mapErr`, `andThen`, `orElse`, `unwrapOr`, and `unwrapOrElse` methods, and change `isOk`/`isErr` to getter properties. Add default error mapper to `run`/`runAsync`, update `runAsync` to call `fn` directly without `.call(null)`, and fix an issue where `null`/`undefined` values were coalescing to the `empty` symbol. Also fix a race condition in `useDelayedToggleState` by clearing timeouts before setting new ones, add a `coverage` entry to `.gitignore`, install `@vitest/coverage-v8`, and add tests for the new Result methods. --- .gitignore | 1 + package.json | 1 + packages/core/package.json | 4 + packages/core/src/result.js | 131 +++++++++++++++++-- packages/core/tests/result.test.js | 157 +++++++++++++++++++++++ packages/react/src/client.js | 24 +++- packages/react/tests/client.test.js | 21 ++++ pnpm-lock.yaml | 187 ++++++++++++++++++++++++++-- 8 files changed, 500 insertions(+), 26 deletions(-) create mode 100644 packages/core/tests/result.test.js diff --git a/.gitignore b/.gitignore index 47dd481..3e8b0a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules typings .direnv/ +coverage diff --git a/package.json b/package.json index 7bf4770..ff13025 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ }, "devDependencies": { "@changesets/cli": "^2.29.8", + "@vitest/coverage-v8": "4.0.18", "oxfmt": "^0.28.0", "oxlint": "^1.43.0", "playwright": "^1.57.0", diff --git a/packages/core/package.json b/packages/core/package.json index 08aaf37..a1191d1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,6 +23,10 @@ "./enum": { "types": "./typings/enum.d.ts", "import": "./src/enum.js" + }, + "./result": { + "types": "./typings/result.d.ts", + "import": "./src/result.js" } }, "scripts": { diff --git a/packages/core/src/result.js b/packages/core/src/result.js index a784c4a..3ca28c7 100644 --- a/packages/core/src/result.js +++ b/packages/core/src/result.js @@ -4,9 +4,44 @@ export const empty = Symbol("empty"); /** - * @template T, E + * Represents the outcome of an operation that can either succeed (`Ok`) or fail (`Err`). + * + * `Result` provides a type-safe way to handle errors functionally without relying on try-catch blocks or throwing exceptions. + * + * ## Falsy Values + * Because the constructor uses the nullish coalescing operator (`?? empty`) to fallback, + * passing `null` or `undefined` will cause them to coalesce to the `empty` symbol. + * Other falsy values (such as `0`, `false`, `""`) are preserved. + * + * @example + * // 1. Creating and unwrapping results + * const success = Result.ok(42); + * if (success.isOk) { + * console.log(success.unwrap()); // 42 + * } + * + * const failure = Result.err(new Error("Something went wrong")); + * if (failure.isErr) { + * console.error(failure.unwrapErr()); // Error: Something went wrong + * } + * + * @example + * // 2. Safe execution using Result.run + * const result = Result.run( + * () => JSON.parse('{"status": "ok"}'), + * (error) => new Error("Invalid JSON: " + error.message) + * ); + * + * @example + * // 3. Transforming results + * const doubled = Result.ok(10) + * .map(x => x * 2) + * .unwrap(); // 20 + * + * @template T The type of the value in the Ok case. + * @template E The type of the error in the Err case. */ -export default class Result { +export class Result { /** @type {T | typeof empty} */ value; @@ -18,21 +53,21 @@ export default class Result { * @param {E | typeof empty} error */ constructor(value, error) { - this.error = error; - this.value = value; + this.error = error ?? empty; + this.value = value ?? empty; } /** * @returns {this is Result} */ - isOk() { + get isOk() { return this.value !== empty; } /** * @returns {this is Result} */ - isErr() { + get isErr() { return this.error !== empty; } @@ -61,6 +96,78 @@ export default class Result { return this.error; } + /** + * Maps a `Result` to `Result` by applying a function to a contained `Ok` value, leaving an `Err` value untouched. + * + * @template U + * @param {(value: T) => U} fn + * @returns {Result} + */ + map(fn) { + if (this.value === empty) return /** @type {any} */ (this); + return Result.ok(fn(this.value)); + } + + /** + * Maps a `Result` to `Result` by applying a function to a contained `Err` value, leaving an `Ok` value untouched. + * + * @template F + * @param {(error: E) => F} fn + * @returns {Result} + */ + mapErr(fn) { + if (this.error === empty) return /** @type {any} */ (this); + return Result.err(fn(this.error)); + } + + /** + * Calls `fn` if the result is `Ok`, otherwise returns the `Err` value of `this`. + * + * @template U, F + * @param {(value: T) => Result} fn + * @returns {Result} + */ + andThen(fn) { + if (this.value === empty) return /** @type {any} */ (this); + return fn(this.value); + } + + /** + * Calls `fn` if the result is `Err`, otherwise returns the `Ok` value of `this`. + * + * @template U, F + * @param {(error: E) => Result} fn + * @returns {Result} + */ + orElse(fn) { + if (this.error === empty) return /** @type {any} */ (this); + return fn(this.error); + } + + /** + * Returns the contained `Ok` value or a provided default. + * + * @template U + * @param {U} defaultValue + * @returns {T | U} + */ + unwrapOr(defaultValue) { + if (this.value === empty) return defaultValue; + return this.value; + } + + /** + * Returns the contained `Ok` value or computes it from a closure. + * + * @template U + * @param {(error: E) => U} fn + * @returns {T | U} + */ + unwrapOrElse(fn) { + if (this.value === empty) return fn(this.error); + return this.value; + } + /** * @template T * @@ -75,7 +182,7 @@ export default class Result { * @template E * * @param {E} error - * @returns {Result} error + * @returns {Result} */ static err(error) { return new Result(empty, error); @@ -85,15 +192,15 @@ export default class Result { * @template T, E * * @param {() => T} fn - * @param {(e: unknown) => E} err + * @param {null | ((e: unknown) => E)} [err=null] * * @returns {Result} */ - static run(fn, err) { + static run(fn, err = null) { try { return Result.ok(fn()); } catch (error) { - return Result.err(err(error)); + return Result.err(err === null ? /** @type {any} */ (error) : err(error)); } } @@ -106,9 +213,9 @@ export default class Result { */ static async runAsync(fn, err = null) { try { - return Result.ok(await fn.call(null)); + return Result.ok(await fn()); } catch (error) { - return Result.err(err === null ? error : err.call(null, error)); + return Result.err(err === null ? /** @type {any} */ (error) : err(error)); } } } diff --git a/packages/core/tests/result.test.js b/packages/core/tests/result.test.js new file mode 100644 index 0000000..72053be --- /dev/null +++ b/packages/core/tests/result.test.js @@ -0,0 +1,157 @@ +import { describe, expect, it } from "vitest"; + +import { Result } from "../src/result.js"; + +describe("Result", () => { + describe("ok", () => { + it("should create an Ok result", () => { + const res = Result.ok(42); + expect(res.isOk).toBe(true); + expect(res.isErr).toBe(false); + expect(res.unwrap()).toBe(42); + expect(() => res.unwrapErr()).toThrow("called `Result.unwrapErr()` on an `Ok` value"); + }); + }); + + describe("err", () => { + it("should create an Err result", () => { + const res = Result.err("some error"); + expect(res.isOk).toBe(false); + expect(res.isErr).toBe(true); + expect(res.unwrapErr()).toBe("some error"); + expect(() => res.unwrap()).toThrow("some error"); + }); + }); + + describe("run", () => { + it("should run successfully and return Ok result", () => { + const res = Result.run(() => 10 + 5); + expect(res.isOk).toBe(true); + expect(res.unwrap()).toBe(15); + }); + + it("should catch errors and return Err result", () => { + const res = Result.run(() => { + throw new Error("fail"); + }); + expect(res.isErr).toBe(true); + expect(res.unwrapErr()).toBeInstanceOf(Error); + expect(/** @type {Error} */ (res.unwrapErr()).message).toBe("fail"); + }); + + it("should map the caught error if mapper is provided", () => { + const res = Result.run( + () => { + throw new Error("fail"); + }, + (err) => (err instanceof Error ? err.message : "unknown"), + ); + expect(res.isErr).toBe(true); + expect(res.unwrapErr()).toBe("fail"); + }); + }); + + describe("runAsync", () => { + it("should run asynchronously successfully and return Ok result", async () => { + const res = await Result.runAsync(async () => 10 + 5); + expect(res.isOk).toBe(true); + expect(res.unwrap()).toBe(15); + }); + + it("should catch async errors and return Err result", async () => { + const res = await Result.runAsync(async () => { + throw new Error("fail async"); + }); + expect(res.isErr).toBe(true); + expect(res.unwrapErr()).toBeInstanceOf(Error); + expect(/** @type {Error} */ (res.unwrapErr()).message).toBe("fail async"); + }); + + it("should map async caught error if mapper is provided", async () => { + const res = await Result.runAsync( + async () => { + throw new Error("fail async"); + }, + (err) => (err instanceof Error ? err.message : "unknown"), + ); + expect(res.isErr).toBe(true); + expect(res.unwrapErr()).toBe("fail async"); + }); + }); + + describe("map", () => { + it("should map Ok value", () => { + const res = Result.ok(10).map((x) => x * 2); + expect(res.unwrap()).toBe(20); + }); + + it("should not map Err value", () => { + const res = Result.err("error").map((x) => x * 2); + expect(res.unwrapErr()).toBe("error"); + }); + }); + + describe("mapErr", () => { + it("should map Err value", () => { + const res = Result.err("error").mapErr((err) => `${err}!`); + expect(res.unwrapErr()).toBe("error!"); + }); + + it("should not map Ok value", () => { + const res = Result.ok(10).mapErr((err) => `${err}!`); + expect(res.unwrap()).toBe(10); + }); + }); + + describe("andThen", () => { + it("should chain Ok result", () => { + const res = Result.ok(10).andThen((x) => Result.ok(x + 5)); + expect(res.unwrap()).toBe(15); + }); + + it("should return the Err result if chaining returning Err", () => { + const res = Result.ok(10).andThen((_) => Result.err("chain error")); + expect(res.isErr).toBe(true); + expect(res.unwrapErr()).toBe("chain error"); + }); + + it("should not chain Err result", () => { + const res = Result.err("original").andThen((x) => Result.ok(x + 5)); + expect(res.unwrapErr()).toBe("original"); + }); + }); + + describe("orElse", () => { + it("should chain Err result", () => { + const res = Result.err("error").orElse((err) => Result.ok(`recovered from ${err}`)); + expect(res.unwrap()).toBe("recovered from error"); + }); + + it("should not chain Ok result", () => { + const res = Result.ok(10).orElse((_) => Result.ok(20)); + expect(res.unwrap()).toBe(10); + }); + }); + + describe("unwrapOr", () => { + it("should return Ok value", () => { + expect(Result.ok(10).unwrapOr(20)).toBe(10); + }); + + it("should return default value on Err", () => { + expect(Result.err("error").unwrapOr(20)).toBe(20); + }); + }); + + describe("unwrapOrElse", () => { + it("should return Ok value", () => { + expect(Result.ok(10).unwrapOrElse(() => 20)).toBe(10); + }); + + it("should return computed value on Err", () => { + expect(Result.err("error").unwrapOrElse((err) => `default ${err}`)).toBe( + "default error", + ); + }); + }); +}); diff --git a/packages/react/src/client.js b/packages/react/src/client.js index a5fa052..079462d 100644 --- a/packages/react/src/client.js +++ b/packages/react/src/client.js @@ -1,5 +1,5 @@ /** @import {Dispatch, SetStateAction} from "react"; */ -import { useCallback, useMemo, useState, useSyncExternalStore } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; /** * Creates a boolean where one gets delayed than the other whenever the setter is called. @@ -19,21 +19,35 @@ export function useDelayedToggleState(defaultValue, delay = 300, escapeDelay = 1 const [toggled, setToggle] = useState(defaultValue); const [delayed, setDelayed] = useState(defaultValue); + const toggleTimeoutRef = useRef(null); + const delayTimeoutRef = useRef(null); + + useEffect(() => { + return () => { + clearTimeout(toggleTimeoutRef.current); + clearTimeout(delayTimeoutRef.current); + }; + }, []); + /** @type {Dispatch>} */ const setState = useCallback( - (value) => + (value) => { + clearTimeout(toggleTimeoutRef.current); + clearTimeout(delayTimeoutRef.current); + setToggle((ctv) => { const x = typeof value === "function" ? value(ctv) : value; if (ctv) { - setTimeout(() => setDelayed(x), delay); + delayTimeoutRef.current = setTimeout(() => setDelayed(x), delay); return x; } setDelayed(x); - setTimeout(() => setToggle(x), escapeDelay); + toggleTimeoutRef.current = setTimeout(() => setToggle(x), escapeDelay); return ctv; - }), + }); + }, [delay, escapeDelay], ); diff --git a/packages/react/tests/client.test.js b/packages/react/tests/client.test.js index 5e35e5c..1ba2701 100644 --- a/packages/react/tests/client.test.js +++ b/packages/react/tests/client.test.js @@ -120,6 +120,27 @@ describe("useDelayedToggleState", () => { expect(result.current[1]).toBe(false); }); + it("should handle actual rapid toggling without race conditions", async () => { + const { result, act } = await renderHook(() => useDelayedToggleState(true, 100)); + + // Start closing (toggled becomes false immediately) + await act(() => result.current[2](false)); + expect(result.current[0]).toBe(false); + expect(result.current[1]).toBe(true); + + // Reopen immediately (after 10ms, before 100ms delay finishes) + await new Promise((resolve) => setTimeout(resolve, 10)); + await act(() => result.current[2](true)); + expect(result.current[1]).toBe(true); + + // Wait for states to settle (past the 100ms delay) + await new Promise((resolve) => setTimeout(resolve, 150)); + + // Since we reopened, both should remain true + expect(result.current[0]).toBe(true); + expect(result.current[1]).toBe(true); + }); + it("should work for dialog/popup use case - opening animation", async () => { const { result, act } = await renderHook(() => useDelayedToggleState(false, 300)); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ab53f8..9349d3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@changesets/cli': specifier: ^2.29.8 version: 2.29.8 + '@vitest/coverage-v8': + specifier: 4.0.18 + version: 4.0.18(@vitest/browser@4.0.18(vite@7.3.1)(vitest@4.0.18))(vitest@4.0.18) oxfmt: specifier: ^0.28.0 version: 0.28.0 @@ -22,7 +25,7 @@ importers: version: 1.57.0 vitest: specifier: ^4.0.18 - version: 4.0.18(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0) + version: 4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0) packages/core: devDependencies: @@ -34,7 +37,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0) + version: 4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0) packages/form-data: devDependencies: @@ -46,7 +49,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0) + version: 4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0) packages/query: devDependencies: @@ -58,7 +61,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0) + version: 4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0) packages/react: dependencies: @@ -92,7 +95,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0) + version: 4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0) vitest-browser-react: specifier: ^2.0.5 version: 2.0.5(@types/react-dom@19.2.3(@types/react@19.2.10))(@types/react@19.2.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.0.18) @@ -153,10 +156,18 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.28.5': resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} @@ -175,6 +186,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -207,6 +223,14 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@changesets/apply-release-plan@7.0.14': resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==} @@ -788,6 +812,15 @@ packages: peerDependencies: vitest: 4.0.18 + '@vitest/coverage-v8@4.0.18': + resolution: {integrity: sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==} + peerDependencies: + '@vitest/browser': 4.0.18 + vitest: 4.0.18 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@4.0.18': resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} @@ -814,6 +847,11 @@ packages: '@vitest/spy@4.0.18': resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/ui@4.0.18': + resolution: {integrity: sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==} + peerDependencies: + vitest: 4.0.18 + '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} @@ -843,6 +881,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-v8-to-istanbul@0.3.12: + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} + baseline-browser-mapping@2.9.19: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true @@ -973,6 +1014,9 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -981,6 +1025,9 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -1014,10 +1061,17 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1064,6 +1118,21 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1114,6 +1183,13 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} @@ -1361,6 +1437,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} @@ -1657,8 +1737,12 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.29.7': {} + '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.28.6': @@ -1674,6 +1758,10 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -1714,6 +1802,13 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@1.0.2': {} + '@changesets/apply-release-plan@7.0.14': dependencies: '@changesets/config': 3.1.2 @@ -2208,7 +2303,7 @@ snapshots: '@vitest/mocker': 4.0.18(vite@7.3.1) playwright: 1.57.0 tinyrainbow: 3.0.3 - vitest: 4.0.18(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0) + vitest: 4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0) transitivePeerDependencies: - bufferutil - msw @@ -2224,7 +2319,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0) + vitest: 4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0) ws: 8.19.0 transitivePeerDependencies: - bufferutil @@ -2232,6 +2327,22 @@ snapshots: - utf-8-validate - vite + '@vitest/coverage-v8@4.0.18(@vitest/browser@4.0.18(vite@7.3.1)(vitest@4.0.18))(vitest@4.0.18)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.0.18 + ast-v8-to-istanbul: 0.3.12 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.1 + std-env: 3.10.0 + tinyrainbow: 3.0.3 + vitest: 4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0) + optionalDependencies: + '@vitest/browser': 4.0.18(vite@7.3.1)(vitest@4.0.18) + '@vitest/expect@4.0.18': dependencies: '@standard-schema/spec': 1.1.0 @@ -2266,6 +2377,18 @@ snapshots: '@vitest/spy@4.0.18': {} + '@vitest/ui@4.0.18(vitest@4.0.18)': + dependencies: + '@vitest/utils': 4.0.18 + fflate: 0.8.3 + flatted: 3.4.2 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vitest: 4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0) + optional: true + '@vitest/utils@4.0.18': dependencies: '@vitest/pretty-format': 4.0.18 @@ -2288,6 +2411,12 @@ snapshots: assertion-error@2.0.1: {} + ast-v8-to-istanbul@0.3.12: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + baseline-browser-mapping@2.9.19: {} better-path-resolve@1.0.0: @@ -2431,6 +2560,9 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.8.3: + optional: true + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2440,6 +2572,9 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + flatted@3.4.2: + optional: true + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2475,6 +2610,8 @@ snapshots: graceful-fs@4.2.11: {} + has-flag@4.0.0: {} + html-encoding-sniffer@6.0.0: dependencies: '@exodus/bytes': 1.11.0 @@ -2482,6 +2619,8 @@ snapshots: - '@noble/hashes' optional: true + html-escaper@2.0.2: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -2525,6 +2664,21 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + js-tokens@10.0.0: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -2590,6 +2744,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + mdn-data@2.12.2: optional: true @@ -2816,6 +2980,10 @@ snapshots: strip-bom@3.0.0: {} + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + symbol-tree@3.2.4: optional: true @@ -2883,12 +3051,12 @@ snapshots: dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - vitest: 4.0.18(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0) + vitest: 4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0) optionalDependencies: '@types/react': 19.2.10 '@types/react-dom': 19.2.3(@types/react@19.2.10) - vitest@4.0.18(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0): + vitest@4.0.18(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jsdom@27.4.0): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(vite@7.3.1) @@ -2912,6 +3080,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@vitest/browser-playwright': 4.0.18(playwright@1.57.0)(vite@7.3.1)(vitest@4.0.18) + '@vitest/ui': 4.0.18(vitest@4.0.18) jsdom: 27.4.0 transitivePeerDependencies: - jiti From b07565282350b4cc7084990ca912c00cb9c42082 Mon Sep 17 00:00:00 2001 From: Curstantine Date: Fri, 17 Jul 2026 23:18:49 +0530 Subject: [PATCH 3/3] chore: create changesets --- .changeset/eager-planets-admire.md | 5 +++++ .changeset/thirty-llamas-invent.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/eager-planets-admire.md create mode 100644 .changeset/thirty-llamas-invent.md diff --git a/.changeset/eager-planets-admire.md b/.changeset/eager-planets-admire.md new file mode 100644 index 0000000..1042ef6 --- /dev/null +++ b/.changeset/eager-planets-admire.md @@ -0,0 +1,5 @@ +--- +"@jabascript/core": minor +--- + +- Added `Result` utility type. diff --git a/.changeset/thirty-llamas-invent.md b/.changeset/thirty-llamas-invent.md new file mode 100644 index 0000000..f6f1dd5 --- /dev/null +++ b/.changeset/thirty-llamas-invent.md @@ -0,0 +1,5 @@ +--- +"@jabascript/react": patch +--- + +- Fix race condition bug in useDelayedToggleState hook by tracking and clearing pending timeouts.