diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3dfd9c5..5e6acbf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,7 +1,10 @@ name: CI on: - - pull_request + pull_request: + push: + branches: + - master env: NODE_VERSION: 22.x diff --git a/.github/workflows/npm-publish.yaml b/.github/workflows/npm-publish.yaml new file mode 100644 index 0000000..8b8364a --- /dev/null +++ b/.github/workflows/npm-publish.yaml @@ -0,0 +1,68 @@ +name: Publish to NPM + +on: + push: + branches: + - main + +env: + NODE_VERSION: 22.x + +jobs: + check-and-publish: + name: Check version changes and publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + run_install: false + + - uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build all packages + run: pnpm all:build + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v46 + with: + files: packages/*/package.json + + - name: Check for version changes and publish + if: steps.changed-files.outputs.any_changed == 'true' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + for file in ${{ steps.changed-files.outputs.all_changed_files }}; do + PACKAGE_DIR=$(dirname $file) + echo "Checking $PACKAGE_DIR for version changes..." + + # Get the latest published version from npm + PACKAGE_NAME=$(node -p "require('./$file').name") + CURRENT_VERSION=$(node -p "require('./$file').version") + + # Check if package exists on npm + NPM_VERSION=$(npm view $PACKAGE_NAME version 2>/dev/null || echo "0.0.0") + + # Compare versions + if [ "$CURRENT_VERSION" != "$NPM_VERSION" ]; then + echo "Version changed for $PACKAGE_NAME: $NPM_VERSION → $CURRENT_VERSION" + echo "Publishing $PACKAGE_NAME@$CURRENT_VERSION" + cd $PACKAGE_DIR + npm publish --access public + cd - + else + echo "No version change detected for $PACKAGE_NAME" + fi + done diff --git a/packages/stdlib/src/index.ts b/packages/stdlib/src/index.ts index 9e936f1..62ebea0 100644 --- a/packages/stdlib/src/index.ts +++ b/packages/stdlib/src/index.ts @@ -5,6 +5,7 @@ export * from './math'; export * from './objects'; export * from './patterns'; export * from './structs'; +export * from './sync'; export * from './text'; export * from './types'; export * from './utils' diff --git a/packages/stdlib/src/sync/index.ts b/packages/stdlib/src/sync/index.ts new file mode 100644 index 0000000..b1ad937 --- /dev/null +++ b/packages/stdlib/src/sync/index.ts @@ -0,0 +1 @@ +export * from './mutex'; diff --git a/packages/stdlib/src/sync/mutex/index.test.ts b/packages/stdlib/src/sync/mutex/index.test.ts new file mode 100644 index 0000000..3b97fc0 --- /dev/null +++ b/packages/stdlib/src/sync/mutex/index.test.ts @@ -0,0 +1,94 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { SyncMutex } from '.'; + +describe('SyncMutex', () => { + let mutex: SyncMutex; + + beforeEach(() => { + mutex = new SyncMutex(); + }); + + it('unlocked by default', () => { + expect(mutex.isLocked).toBe(false); + }); + + it('lock the mutex', () => { + mutex.lock(); + + expect(mutex.isLocked).toBe(true); + }); + + it('remain locked when locked multiple times', () => { + mutex.lock(); + mutex.lock(); + + expect(mutex.isLocked).toBe(true); + }); + + it('unlock a locked mutex', () => { + mutex.lock(); + mutex.unlock(); + + expect(mutex.isLocked).toBe(false); + }); + + it('remain unlocked when unlocked multiple times', () => { + mutex.unlock(); + mutex.unlock(); + + expect(mutex.isLocked).toBe(false); + }); + + it('reflect the current lock state', () => { + expect(mutex.isLocked).toBe(false); + mutex.lock(); + expect(mutex.isLocked).toBe(true); + mutex.unlock(); + expect(mutex.isLocked).toBe(false); + }); + + it('execute a callback when unlocked', async () => { + const callback = vi.fn(() => 'done'); + const result = await mutex.execute(callback); + + expect(result).toBe('done'); + expect(callback).toHaveBeenCalled(); + }); + + it('execute a promise callback when unlocked', async () => { + const callback = vi.fn(() => Promise.resolve('done')); + const result = await mutex.execute(callback); + + expect(result).toBe('done'); + expect(callback).toHaveBeenCalled(); + }); + + it('execute concurrent callbacks only one at a time', async () => { + const callback = vi.fn(() => Promise.resolve('done')); + + const result = await Promise.all([ + mutex.execute(callback), + mutex.execute(callback), + mutex.execute(callback), + ]); + + expect(result).toEqual(['done', undefined, undefined]); + expect(callback).toHaveBeenCalledTimes(1); + }); + + it('does not execute a callback when locked', async () => { + const callback = vi.fn(() => 'done'); + mutex.lock(); + const result = await mutex.execute(callback); + + expect(result).toBeUndefined(); + expect(callback).not.toHaveBeenCalled(); + }); + + it('unlocks after executing a callback', async () => { + const callback = vi.fn(() => 'done'); + await mutex.execute(callback); + + expect(mutex.isLocked).toBe(false); + }); +}); diff --git a/packages/stdlib/src/sync/mutex/index.ts b/packages/stdlib/src/sync/mutex/index.ts new file mode 100644 index 0000000..991bc19 --- /dev/null +++ b/packages/stdlib/src/sync/mutex/index.ts @@ -0,0 +1,47 @@ +import type { MaybePromise } from "../../types"; + +/** + * @name SyncMutex + * @category Utils + * @description A simple synchronous mutex to provide more readable locking and unlocking of code blocks + * + * @example + * const mutex = new SyncMutex(); + * + * mutex.lock(); + * + * mutex.unlock(); + * + * const result = await mutex.execute(() => { + * // do something + * return Promise.resolve('done'); + * }); + * + * @since 0.0.5 + */ +export class SyncMutex { + private state = false; + + public get isLocked() { + return this.state; + } + + public lock() { + this.state = true; + } + + public unlock() { + this.state = false; + } + + public async execute(callback: () => T) { + if (this.isLocked) + return; + + this.lock(); + const result = await callback(); + this.unlock(); + + return result; + } +} diff --git a/packages/vue/src/composables/useRenderInfo/index.ts b/packages/vue/src/composables/useRenderInfo/index.ts index e7518c8..bbb4a1f 100644 --- a/packages/vue/src/composables/useRenderInfo/index.ts +++ b/packages/vue/src/composables/useRenderInfo/index.ts @@ -22,9 +22,13 @@ import { getLifeCycleTarger } from '../..'; export function useRenderInfo(instance?: ComponentInternalInstance) { const target = getLifeCycleTarger(instance); const duration = ref(0); + let renderStartTime = 0; - const startMark = () => duration.value = performance.now(); - const endMark = () => duration.value = Math.max(performance.now() - duration.value, 0); + const startMark = () => renderStartTime = performance.now(); + const endMark = () => { + duration.value = Math.max(performance.now() - renderStartTime, 0); + renderStartTime = 0; + }; onBeforeMount(startMark, target); onMounted(endMark, target);