1
0
mirror of https://github.com/robonen/tools.git synced 2026-03-20 10:54:44 +00:00

2 Commits

19 changed files with 2679 additions and 2454 deletions

View File

@@ -16,14 +16,14 @@ jobs:
contents: read contents: read
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
with: with:
run_install: false run_install: false
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: pnpm cache: pnpm

View File

@@ -13,7 +13,7 @@ jobs:
name: Check version changes and publish name: Check version changes and publish
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -22,7 +22,7 @@ jobs:
with: with:
run_install: false run_install: false
- uses: actions/setup-node@v6 - uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
cache: pnpm cache: pnpm

View File

@@ -15,9 +15,9 @@
"url": "git+https://github.com/robonen/tools.git", "url": "git+https://github.com/robonen/tools.git",
"directory": "packages/tsconfig" "directory": "packages/tsconfig"
}, },
"packageManager": "pnpm@10.20.0", "packageManager": "pnpm@10.13.1",
"engines": { "engines": {
"node": ">=24.11.0" "node": ">=22.17.1"
}, },
"files": [ "files": [
"**tsconfig.json" "**tsconfig.json"

View File

@@ -18,9 +18,9 @@
"url": "git+https://github.com/robonen/tools.git", "url": "git+https://github.com/robonen/tools.git",
"directory": "packages/platform" "directory": "packages/platform"
}, },
"packageManager": "pnpm@10.20.0", "packageManager": "pnpm@10.13.1",
"engines": { "engines": {
"node": ">=24.11.0" "node": ">=22.17.1"
}, },
"type": "module", "type": "module",
"files": [ "files": [

View File

@@ -18,9 +18,9 @@
"url": "git+https://github.com/robonen/tools.git", "url": "git+https://github.com/robonen/tools.git",
"directory": "packages/stdlib" "directory": "packages/stdlib"
}, },
"packageManager": "pnpm@10.20.0", "packageManager": "pnpm@10.13.1",
"engines": { "engines": {
"node": ">=24.11.0" "node": ">=22.17.1"
}, },
"type": "module", "type": "module",
"files": [ "files": [

View File

@@ -1,2 +1,3 @@
export * from './retry';
export * from './sleep'; export * from './sleep';
export * from './tryIt'; export * from './tryIt';

View File

@@ -0,0 +1,250 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { retry } from '.';
describe('retry', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('return the result on first successful attempt', async () => {
const successFn = vi.fn().mockResolvedValue('success');
const result = await retry(successFn);
expect(result).toBe('success');
expect(successFn).toHaveBeenCalledTimes(1);
expect(successFn).toHaveBeenCalledWith({ count: 1, stop: expect.any(Function) });
});
it('use default times value of 2', async () => {
const failingFn = vi.fn().mockRejectedValue(new Error('Test error'));
await expect(retry(failingFn)).rejects.toThrow('Test error');
expect(failingFn).toHaveBeenCalledTimes(2);
});
it('retry the specified number of times on failure', async () => {
const failingFn = vi.fn().mockRejectedValue(new Error('Test error'));
await expect(retry(failingFn, { times: 3 })).rejects.toThrow('Test error');
expect(failingFn).toHaveBeenCalledTimes(3);
expect(failingFn).toHaveBeenNthCalledWith(1, { count: 1, stop: expect.any(Function) });
expect(failingFn).toHaveBeenNthCalledWith(2, { count: 2, stop: expect.any(Function) });
expect(failingFn).toHaveBeenNthCalledWith(3, { count: 3, stop: expect.any(Function) });
});
it('succeed on the last attempt', async () => {
const partiallyFailingFn = vi.fn()
.mockRejectedValueOnce(new Error('First failure'))
.mockRejectedValueOnce(new Error('Second failure'))
.mockResolvedValue('success');
const result = await retry(partiallyFailingFn, { times: 3 });
expect(result).toBe('success');
expect(partiallyFailingFn).toHaveBeenCalledTimes(3);
});
it('use custom shouldRetry function', async () => {
const networkError = new Error('Network failed');
networkError.name = 'NetworkError';
const failingFn = vi.fn().mockRejectedValue(networkError);
await expect(retry(failingFn, {
times: 3,
shouldRetry: (error) => error.name !== 'NetworkError'
})).rejects.toThrow('Network failed');
expect(failingFn).toHaveBeenCalledTimes(1);
});
it('retry with custom shouldRetry based on count', async () => {
const testError = new Error('Test error');
const failingFn = vi.fn().mockRejectedValue(testError);
await expect(retry(failingFn, {
times: 5,
shouldRetry: (error, count) => count < 3 // Only retry first 2 attempts
})).rejects.toThrow('Test error');
expect(failingFn).toHaveBeenCalledTimes(3); // Initial + 2 retries
});
it('retry specific error types with custom shouldRetry', async () => {
const temporaryError = new Error('Temporary failure');
temporaryError.name = 'TemporaryError';
const permanentError = new Error('Permanent failure');
permanentError.name = 'PermanentError';
const failingFn = vi.fn()
.mockRejectedValueOnce(temporaryError)
.mockRejectedValueOnce(temporaryError)
.mockRejectedValueOnce(permanentError);
await expect(retry(failingFn, {
times: 5,
shouldRetry: (error) => error.name === 'TemporaryError'
})).rejects.toThrow('Permanent failure');
expect(failingFn).toHaveBeenCalledTimes(3);
});
it('wait for the specified delay between retries', async () => {
const failingFn = vi.fn().mockRejectedValue(new Error('Test error'));
const retryPromise = retry(failingFn, { times: 3, delay: 1000 });
// First call should happen immediately
expect(failingFn).toHaveBeenCalledTimes(1);
// Advance time to trigger first retry
await vi.advanceTimersByTimeAsync(1000);
expect(failingFn).toHaveBeenCalledTimes(2);
// Advance time to trigger second retry
await vi.advanceTimersByTimeAsync(1000);
expect(failingFn).toHaveBeenCalledTimes(3);
await expect(retryPromise).rejects.toThrow('Test error');
});
it('use dynamic delay function', async () => {
const failingFn = vi.fn().mockRejectedValue(new Error('Test error'));
const delayFn = vi.fn((count: number) => count * 500);
const retryPromise = retry(failingFn, { times: 3, delay: delayFn });
// First call should happen immediately
expect(failingFn).toHaveBeenCalledTimes(1);
// First retry should wait for delay(2) = 1000ms
await vi.advanceTimersByTimeAsync(1000);
expect(failingFn).toHaveBeenCalledTimes(2);
expect(delayFn).toHaveBeenCalledWith(2);
// Second retry should wait for delay(3) = 1500ms
await vi.advanceTimersByTimeAsync(1500);
expect(failingFn).toHaveBeenCalledTimes(3);
expect(delayFn).toHaveBeenCalledWith(3);
await expect(retryPromise).rejects.toThrow('Test error');
});
it('not delay after the last attempt', async () => {
const failingFn = vi.fn().mockRejectedValue(new Error('Test error'));
const retryPromise = retry(failingFn, { times: 2, delay: 1000 });
// Wait for the first retry delay
await vi.advanceTimersByTimeAsync(1000);
// Should complete without further delays
await expect(retryPromise).rejects.toThrow('Test error');
expect(failingFn).toHaveBeenCalledTimes(2);
});
it('handle zero delay', async () => {
const failingFn = vi.fn().mockRejectedValue(new Error('Test error'));
await expect(retry(failingFn, { times: 3, delay: 0 })).rejects.toThrow('Test error');
expect(failingFn).toHaveBeenCalledTimes(3);
});
it('pass the count parameter to the function', async () => {
const countingFn = vi.fn(async ({ count }: { count: number }) => {
if (count < 3) {
throw new Error(`Attempt ${count} failed`);
}
return `Success on attempt ${count}`;
});
const result = await retry(countingFn, { times: 3 });
expect(result).toBe('Success on attempt 3');
expect(countingFn).toHaveBeenCalledWith({ count: 1, stop: expect.any(Function) });
expect(countingFn).toHaveBeenCalledWith({ count: 2, stop: expect.any(Function) });
expect(countingFn).toHaveBeenCalledWith({ count: 3, stop: expect.any(Function) });
});
it('throw the last error when all attempts fail', async () => {
const firstError = new Error('First error');
const lastError = new Error('Last error');
const failingFn = vi.fn()
.mockRejectedValueOnce(firstError)
.mockRejectedValueOnce(lastError);
await expect(retry(failingFn, { times: 2 })).rejects.toThrow('Last error');
});
it('handle times value of 1', async () => {
const failingFn = vi.fn().mockRejectedValue(new Error('Test error'));
await expect(retry(failingFn, { times: 1 })).rejects.toThrow('Test error');
expect(failingFn).toHaveBeenCalledTimes(1);
});
it('handle function that returns non-promise values', async () => {
const syncFn = vi.fn(async ({ count }: { count: number }) => {
if (count === 1) {
throw new Error('First attempt failed');
}
return 'success';
});
const result = await retry(syncFn, { times: 2 });
expect(result).toBe('success');
expect(syncFn).toHaveBeenCalledTimes(2);
});
it('handle complex return types', async () => {
const complexFn = vi.fn().mockResolvedValue({
data: [1, 2, 3],
status: 'ok',
metadata: { timestamp: 123456 }
});
const result = await retry(complexFn);
expect(result).toEqual({
data: [1, 2, 3],
status: 'ok',
metadata: { timestamp: 123456 }
});
});
it('stop retrying when stop function is called', async () => {
const customError = new Error('Custom stop error');
const stopFn = vi.fn(async ({ count, stop }: { count: number, stop: (error: any) => void }) => {
if (count === 2) {
stop(customError);
}
throw new Error(`Attempt ${count} failed`);
});
await expect(retry(stopFn, { times: 5 })).rejects.toThrow('Custom stop error');
expect(stopFn).toHaveBeenCalledTimes(2);
});
it('stop retrying with undefined error when stop is called without argument', async () => {
const stopFn = vi.fn(async ({ count, stop }: { count: number, stop: (error?: any) => void }) => {
if (count === 2) {
stop();
}
throw new Error(`Attempt ${count} failed`);
});
await expect(retry(stopFn, { times: 5 })).rejects.toBeUndefined();
expect(stopFn).toHaveBeenCalledTimes(2);
});
});

View File

@@ -1,9 +1,22 @@
import { tryIt } from '../tryIt';
import { sleep } from '../sleep';
import { isFunction } from '../../types';
export interface RetryOptions { export interface RetryOptions {
times?: number; times?: number;
delay?: number; delay?: number | ((count: number) => number);
backoff: (options: RetryOptions & { count: number }) => number; shouldRetry?: (error: Error, count: number) => boolean;
} }
export type RetryFunction<Return> = (
args: {
count: number;
stop: (error: any) => void;
},
) => Promise<Return>;
const RetryEarlyExit = Symbol('RetryEarlyExit');
/** /**
* @name retry * @name retry
* @category Async * @category Async
@@ -25,14 +38,51 @@ export interface RetryOptions {
* .then(response => response.json()) * .then(response => response.json())
* }, { times: 3, delay: 1000 }); * }, { times: 3, delay: 1000 });
* *
* @since 0.0.8
*/ */
export async function retry<Return>( export async function retry<Return>(
fn: () => Promise<Return>, fn: RetryFunction<Return>,
options: RetryOptions options: RetryOptions = {},
) { ): Promise<Return> {
const { const {
times = 3, times = 2,
delay = 0,
shouldRetry,
} = options; } = options;
let count = 0; let count = 1;
let lastError: Error = new Error('Retry failed');
while (count <= times) {
const metadata = {
count,
stop: (error?: any) => {
throw { [RetryEarlyExit]: error };
},
};
const { error, data } = await tryIt(fn)(metadata);
if (!error)
return data;
if (RetryEarlyExit in error)
throw error[RetryEarlyExit];
if (shouldRetry && !shouldRetry(error, count))
throw error;
lastError = error;
count++;
// Don't delay after the last attempt
if (count <= times) {
const delayMs = isFunction(delay) ? delay(count) : delay;
if (delayMs > 0)
await sleep(delayMs);
}
}
throw lastError;
} }

View File

@@ -6,62 +6,62 @@ describe('tryIt', () => {
const syncFn = (x: number) => x * 2; const syncFn = (x: number) => x * 2;
const wrappedSyncFn = tryIt(syncFn); const wrappedSyncFn = tryIt(syncFn);
const [error, result] = wrappedSyncFn(2); const { error, data } = wrappedSyncFn(2);
expect(error).toBeUndefined(); expect(error).toBeUndefined();
expect(result).toBe(4); expect(data).toBe(4);
}); });
it('handle synchronous functions with errors', () => { it('handle synchronous functions with errors', () => {
const syncFn = (): void => { throw new Error('Test error') }; const syncFn = (): void => { throw new Error('Test error') };
const wrappedSyncFn = tryIt(syncFn); const wrappedSyncFn = tryIt(syncFn);
const [error, result] = wrappedSyncFn(); const { error, data } = wrappedSyncFn();
expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(Error);
expect(error?.message).toBe('Test error'); expect(error?.message).toBe('Test error');
expect(result).toBeUndefined(); expect(data).toBeUndefined();
}); });
it('handle asynchronous functions without errors', async () => { it('handle asynchronous functions without errors', async () => {
const asyncFn = async (x: number) => x * 2; const asyncFn = async (x: number) => x * 2;
const wrappedAsyncFn = tryIt(asyncFn); const wrappedAsyncFn = tryIt(asyncFn);
const [error, result] = await wrappedAsyncFn(2); const { error, data } = await wrappedAsyncFn(2);
expect(error).toBeUndefined(); expect(error).toBeUndefined();
expect(result).toBe(4); expect(data).toBe(4);
}); });
it('handle asynchronous functions with errors', async () => { it('handle asynchronous functions with errors', async () => {
const asyncFn = async () => { throw new Error('Test error') }; const asyncFn = async () => { throw new Error('Test error') };
const wrappedAsyncFn = tryIt(asyncFn); const wrappedAsyncFn = tryIt(asyncFn);
const [error, result] = await wrappedAsyncFn(); const { error, data } = await wrappedAsyncFn();
expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(Error);
expect(error?.message).toBe('Test error'); expect(error?.message).toBe('Test error');
expect(result).toBeUndefined(); expect(data).toBeUndefined();
}); });
it('handle promise-based functions without errors', async () => { it('handle promise-based functions without errors', async () => {
const promiseFn = (x: number) => Promise.resolve(x * 2); const promiseFn = (x: number) => Promise.resolve(x * 2);
const wrappedPromiseFn = tryIt(promiseFn); const wrappedPromiseFn = tryIt(promiseFn);
const [error, result] = await wrappedPromiseFn(2); const { error, data } = await wrappedPromiseFn(2);
expect(error).toBeUndefined(); expect(error).toBeUndefined();
expect(result).toBe(4); expect(data).toBe(4);
}); });
it('handle promise-based functions with errors', async () => { it('handle promise-based functions with errors', async () => {
const promiseFn = () => Promise.reject(new Error('Test error')); const promiseFn = () => Promise.reject(new Error('Test error'));
const wrappedPromiseFn = tryIt(promiseFn); const wrappedPromiseFn = tryIt(promiseFn);
const [error, result] = await wrappedPromiseFn(); const { error, data } = await wrappedPromiseFn();
expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(Error);
expect(error?.message).toBe('Test error'); expect(error?.message).toBe('Test error');
expect(result).toBeUndefined(); expect(data).toBeUndefined();
}); });
}); });

View File

@@ -1,8 +1,8 @@
import { isPromise } from '../../types'; import { isPromise } from '../../types';
export type TryItReturn<Return> = Return extends Promise<any> export type TryItReturn<Return> = Return extends Promise<any>
? Promise<[Error, undefined] | [undefined, Awaited<Return>]> ? Promise<{ error: Error; data: undefined } | { error: undefined; data: Awaited<Return> }>
: [Error, undefined] | [undefined, Return]; : { error: Error; data: undefined } | { error: undefined; data: Return };
/** /**
* @name tryIt * @name tryIt
@@ -14,10 +14,10 @@ export type TryItReturn<Return> = Return extends Promise<any>
* *
* @example * @example
* const wrappedFetch = tryIt(fetch); * const wrappedFetch = tryIt(fetch);
* const [error, result] = await wrappedFetch('https://jsonplaceholder.typicode.com/todos/1'); * const { error, data } = await wrappedFetch('https://jsonplaceholder.typicode.com/todos/1');
* *
* @example * @example
* const [error, result] = await tryIt(fetch)('https://jsonplaceholder.typicode.com/todos/1'); * const { error, data } = await tryIt(fetch)('https://jsonplaceholder.typicode.com/todos/1');
* *
* @since 0.0.3 * @since 0.0.3
*/ */
@@ -30,12 +30,12 @@ export function tryIt<Args extends any[], Return>(
if (isPromise(result)) if (isPromise(result))
return result return result
.then((value) => [undefined, value]) .then((value) => ({ error: undefined, data: value }))
.catch((error) => [error, undefined]) as TryItReturn<Return>; .catch((error) => ({ error, data: undefined })) as TryItReturn<Return>;
return [undefined, result] as TryItReturn<Return>; return { error: undefined, data: result } as TryItReturn<Return>;
} catch (error) { } catch (error) {
return [error, undefined] as TryItReturn<Return>; return { error, data: undefined } as TryItReturn<Return>;
} }
}; };
} }

View File

@@ -16,9 +16,9 @@
"url": "git+https://github.com/robonen/tools.git", "url": "git+https://github.com/robonen/tools.git",
"directory": "packages/renovate" "directory": "packages/renovate"
}, },
"packageManager": "pnpm@10.20.0", "packageManager": "pnpm@10.13.1",
"engines": { "engines": {
"node": ">=24.11.0" "node": ">=22.17.1"
}, },
"files": [ "files": [
"default.json" "default.json"
@@ -27,6 +27,6 @@
"test": "renovate-config-validator ./default.json" "test": "renovate-config-validator ./default.json"
}, },
"devDependencies": { "devDependencies": {
"renovate": "^41.169.1" "renovate": "^41.43.5"
} }
} }

View File

@@ -15,20 +15,20 @@
"type": "git", "type": "git",
"url": "git+https://github.com/robonen/tools.git" "url": "git+https://github.com/robonen/tools.git"
}, },
"packageManager": "pnpm@10.20.0", "packageManager": "pnpm@10.13.1",
"engines": { "engines": {
"node": ">=24.11.0" "node": ">=22.17.1"
}, },
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/node": "^24.10.0", "@types/node": "^22.16.5",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
"citty": "^0.1.6", "citty": "^0.1.6",
"jiti": "^2.6.1", "jiti": "^2.5.1",
"jsdom": "catalog:",
"scule": "^1.3.0", "scule": "^1.3.0",
"vitest": "catalog:" "jsdom": "catalog:",
"vitest": "catalog:",
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:"
}, },
"scripts": { "scripts": {
"build": "pnpm -r build", "build": "pnpm -r build",

4575
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,11 +5,11 @@ packages:
- web/* - web/*
catalog: catalog:
'@vitest/coverage-v8': ^4.0.9 '@vitest/coverage-v8': ^3.2.4
'@vue/test-utils': ^2.4.6 '@vue/test-utils': ^2.4.6
jsdom: ^27.1.0 jsdom: ^26.1.0
pathe: ^2.0.3 pathe: ^2.0.3
unbuild: 3.6.1 unbuild: 3.6.0
vitest: ^4.0.9 vitest: ^3.2.4
'@vitest/ui': ^4.0.9 '@vitest/ui': ^3.2.4
vue: ^3.5.22 vue: ^3.5.18

View File

@@ -1,6 +1,6 @@
{ {
"name": "@robonen/vue", "name": "@robonen/vue",
"version": "0.0.11", "version": "0.0.9",
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "Collection of powerful tools for Vue", "description": "Collection of powerful tools for Vue",
"keywords": [ "keywords": [
@@ -16,9 +16,9 @@
"url": "git+https://github.com/robonen/tools.git", "url": "git+https://github.com/robonen/tools.git",
"directory": "./packages/vue" "directory": "./packages/vue"
}, },
"packageManager": "pnpm@10.20.0", "packageManager": "pnpm@10.13.1",
"engines": { "engines": {
"node": ">=24.11.0" "node": ">=22.17.1"
}, },
"type": "module", "type": "module",
"files": [ "files": [
@@ -37,13 +37,13 @@
"build": "unbuild" "build": "unbuild"
}, },
"devDependencies": { "devDependencies": {
"@robonen/platform": "workspace:*",
"@robonen/stdlib": "workspace:*",
"@robonen/tsconfig": "workspace:*", "@robonen/tsconfig": "workspace:*",
"@vue/test-utils": "catalog:", "@vue/test-utils": "catalog:",
"unbuild": "catalog:" "unbuild": "catalog:"
}, },
"dependencies": { "dependencies": {
"@robonen/platform": "workspace:*",
"@robonen/stdlib": "workspace:*",
"vue": "catalog:" "vue": "catalog:"
} }
} }

View File

@@ -1,61 +0,0 @@
import { describe, expect, it } from 'vitest';
import { computed, defineComponent, nextTick, ref, shallowRef } from 'vue';
import { mount } from '@vue/test-utils'
import { unrefElement } from '.';
describe('unrefElement', () => {
it('returns a plain element when passed a raw element', () => {
const htmlEl = document.createElement('div');
const svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
expect(unrefElement(htmlEl)).toBe(htmlEl);
expect(unrefElement(svgEl)).toBe(svgEl);
});
it('returns element when passed a ref or shallowRef to an element', () => {
const el = document.createElement('div');
const elRef = ref<HTMLElement | null>(el);
const shallowElRef = shallowRef<HTMLElement | null>(el);
expect(unrefElement(elRef)).toBe(el);
expect(unrefElement(shallowElRef)).toBe(el);
});
it('returns element when passed a computed ref or getter function', () => {
const el = document.createElement('div');
const computedElRef = computed(() => el);
const elGetter = () => el;
expect(unrefElement(computedElRef)).toBe(el);
expect(unrefElement(elGetter)).toBe(el);
});
it('returns component $el when passed a component instance', async () => {
const Child = defineComponent({
template: `<span class="child-el">child</span>`,
});
const Parent = defineComponent({
components: { Child },
template: `<Child ref="childRef" />`,
});
const wrapper = mount(Parent);
await nextTick();
const childInstance = (wrapper.vm as any).$refs.childRef;
const result = unrefElement(childInstance);
expect(result).toBe(childInstance.$el);
expect((result as HTMLElement).classList.contains('child-el')).toBe(true);
});
it('handles null and undefined values', () => {
expect(unrefElement(undefined)).toBe(undefined);
expect(unrefElement(null)).toBe(null);
expect(unrefElement(ref(null))).toBe(null);
expect(unrefElement(ref(undefined))).toBe(undefined);
expect(unrefElement(() => null)).toBe(null);
expect(unrefElement(() => undefined)).toBe(undefined);
});
});

View File

@@ -1,33 +0,0 @@
import type { ComponentPublicInstance, MaybeRef, MaybeRefOrGetter } from 'vue';
import { toValue } from 'vue';
export type VueInstance = ComponentPublicInstance;
export type MaybeElement = HTMLElement | SVGElement | VueInstance | undefined | null;
export type MaybeElementRef<El extends MaybeElement = MaybeElement> = MaybeRef<El>;
export type MaybeComputedElementRef<El extends MaybeElement = MaybeElement> = MaybeRefOrGetter<El>;
export type UnRefElementReturn<T extends MaybeElement = MaybeElement> = T extends VueInstance ? Exclude<MaybeElement, VueInstance> : T | undefined;
/**
* @name unrefElement
* @category Components
* @description Unwraps a Vue element reference to get the underlying instance or DOM element.
*
* @param {MaybeComputedElementRef<El>} elRef - The element reference to unwrap.
* @returns {UnRefElementReturn<El>} - The unwrapped element or undefined.
*
* @example
* const element = useTemplateRef<HTMLElement>('element');
* const result = unrefElement(element); // result is the element instance
*
* @example
* const component = useTemplateRef<Component>('component');
* const result = unrefElement(component); // result is the component instance
*
* @since 0.0.11
*/
export function unrefElement<El extends MaybeElement>(elRef: MaybeComputedElementRef<El>): UnRefElementReturn<El> {
const plain = toValue(elRef);
return (plain as VueInstance)?.$el ?? plain;
}

View File

@@ -1,4 +1,4 @@
import { inject as vueInject, provide as vueProvide, type InjectionKey, type App } from 'vue'; import { inject, provide, type InjectionKey, type App } from 'vue';
import { VueToolsError } from '../..'; import { VueToolsError } from '../..';
/** /**
@@ -34,8 +34,8 @@ import { VueToolsError } from '../..';
export function useContextFactory<ContextValue>(name: string) { export function useContextFactory<ContextValue>(name: string) {
const injectionKey: InjectionKey<ContextValue> = Symbol(name); const injectionKey: InjectionKey<ContextValue> = Symbol(name);
const inject = <Fallback extends ContextValue = ContextValue>(fallback?: Fallback) => { const injectContext = <Fallback extends ContextValue = ContextValue>(fallback?: Fallback) => {
const context = vueInject(injectionKey, fallback); const context = inject(injectionKey, fallback);
if (context !== undefined) if (context !== undefined)
return context; return context;
@@ -43,8 +43,8 @@ export function useContextFactory<ContextValue>(name: string) {
throw new VueToolsError(`useContextFactory: '${name}' context is not provided`); throw new VueToolsError(`useContextFactory: '${name}' context is not provided`);
}; };
const provide = (context: ContextValue) => { const provideContext = (context: ContextValue) => {
vueProvide(injectionKey, context); provide(injectionKey, context);
return context; return context;
}; };
@@ -54,8 +54,8 @@ export function useContextFactory<ContextValue>(name: string) {
}; };
return { return {
inject, inject: injectContext,
provide, provide: provideContext,
appProvide, appProvide,
key: injectionKey, key: injectionKey,
} }

View File

@@ -1,8 +1,7 @@
import { useContextFactory } from '../useContextFactory'; import { inject, provide, type App, type InjectionKey } from 'vue';
import type { App, InjectionKey } from 'vue';
export interface useInjectionStoreOptions<Return> { export interface useInjectionStoreOptions<Return> {
injectionName?: string; injectionKey: string | InjectionKey<Return>;
defaultValue?: Return; defaultValue?: Return;
} }
@@ -47,23 +46,23 @@ export interface useInjectionStoreOptions<Return> {
*/ */
export function useInjectionStore<Args extends any[], Return>( export function useInjectionStore<Args extends any[], Return>(
stateFactory: (...args: Args) => Return, stateFactory: (...args: Args) => Return,
options?: useInjectionStoreOptions<Return> options?: useInjectionStoreOptions<Return>,
) { ) {
const ctx = useContextFactory<Return>(options?.injectionName ?? stateFactory.name ?? 'InjectionStore'); const key = options?.injectionKey ?? Symbol(stateFactory.name ?? 'InjectionStore');
const useProvidingState = (...args: Args) => { const useProvidingState = (...args: Args) => {
const state = stateFactory(...args); const state = stateFactory(...args);
ctx.provide(state); provide(key, state);
return state; return state;
}; };
const useAppProvidingState = (app: App) => (...args: Args) => { const useAppProvidingState = (app: App) => (...args: Args) => {
const state = stateFactory(...args); const state = stateFactory(...args);
ctx.appProvide(app)(state); app.provide(key, state);
return state; return state;
}; };
const useInjectedState = () => ctx.inject(options?.defaultValue); const useInjectedState = () => inject(key, options?.defaultValue);
return { return {
useProvidingState, useProvidingState,