-
Notifications
You must be signed in to change notification settings - Fork 230
Fix data hooks and adapter edge cases #3765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,8 @@ | |
| // ───────────────────────────────────────────────────── | ||
|
|
||
| import type { Options } from 'execa'; | ||
| import { execa } from 'execa'; | ||
|
|
||
| type ExecaFunction = (file: string, args?: string[], options?: Options) => any; | ||
|
|
||
| export interface UseExecaResult { | ||
| run( | ||
|
|
@@ -32,15 +33,15 @@ async function getExeca(): Promise<typeof import('execa')> { | |
| } | ||
| } | ||
|
|
||
| function isExecaCallable(obj: unknown): obj is typeof execa { | ||
| function isExecaCallable(obj: unknown): obj is ExecaFunction { | ||
| return typeof obj === 'function'; | ||
| } | ||
|
|
||
| function isExecaModuleWithExeca(obj: unknown): obj is { execa: typeof execa } { | ||
| function isExecaModuleWithExeca(obj: unknown): obj is { execa: ExecaFunction } { | ||
| return typeof obj === 'object' && obj !== null && 'execa' in obj && typeof (obj as { execa: unknown }).execa === 'function'; | ||
|
Comment on lines
+40
to
41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
file="packages/adapters/src/execa/index.ts"
printf '%s\n' '--- file ---'
cat -n "$file"
printf '%s\n' '--- nearby type assertions and any usages ---'
rg -n -C 2 '\bas\s+|\bany\b|isExecaModuleWithExeca' "$file"Repository: Karanjot786/TermUI Length of output: 6841 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- TypeScript compiler availability ---'
if command -v tsc >/dev/null 2>&1; then
tsc --version
cat >/tmp/execa-guard-check.ts <<'TS'
function isExecaModuleWithExeca(
obj: unknown,
): obj is { execa: (...args: never[]) => unknown } {
return (
typeof obj === 'object' &&
obj !== null &&
'execa' in obj &&
typeof obj.execa === 'function'
);
}
TS
tsc --strict --noEmit --skipLibCheck /tmp/execa-guard-check.ts
printf '%s\n' 'strict property-narrowing check passed'
else
printf '%s\n' 'tsc is unavailable'
fi
printf '%s\n' '--- TypeScript configuration files ---'
find . -maxdepth 3 -type f \( -name 'tsconfig*.json' -o -name 'package.json' \) -printRepository: Karanjot786/TermUI Length of output: 3865 Remove the undocumented assertion and After the 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
|
|
||
| function isExecaModuleWithDefault(obj: unknown): obj is { default: typeof execa } { | ||
| function isExecaModuleWithDefault(obj: unknown): obj is { default: ExecaFunction } { | ||
| return typeof obj === 'object' && obj !== null && 'default' in obj && typeof (obj as { default: unknown }).default === 'function'; | ||
| } | ||
|
|
||
|
|
@@ -60,7 +61,7 @@ export function useExeca(globalOpts?: Options): UseExecaResult { | |
| opts?: Options | ||
| ): AsyncGenerator<string, void, unknown> { | ||
| const execaModule: unknown = await getExeca(); | ||
| let execaFn: typeof execa; | ||
| let execaFn: ExecaFunction; | ||
|
|
||
| if (isExecaModuleWithExeca(execaModule)) { | ||
| execaFn = execaModule.execa; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -79,6 +79,63 @@ describe('useMutation', () => { | |
| expect(returnData).toEqual(mockResponseData); | ||
| }); | ||
|
|
||
| it('treats successful empty responses as completed mutations', async () => { | ||
| mockFetch.mockResolvedValue({ | ||
| ok: true, | ||
| status: 204, | ||
| text: async () => '', | ||
| }); | ||
|
|
||
| render(createElement(TestComponent, { url: '/api/test', method: 'DELETE' })); | ||
|
|
||
| const returnData = await (global as any).hookResult.mutate(undefined); | ||
| await flushPromises(); | ||
|
|
||
| const result = (global as any).hookResult; | ||
| expect(returnData).toBeNull(); | ||
| expect(result.loading).toBe(false); | ||
| expect(result.data).toBeNull(); | ||
| expect(result.error).toBeNull(); | ||
| expect(result.mutationCount).toBe(1); | ||
| }); | ||
|
|
||
| it('does not let older concurrent mutations overwrite newer state', async () => { | ||
| let resolveFirst!: (response: any) => void; | ||
| let resolveSecond!: (response: any) => void; | ||
|
Comment on lines
+103
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
file="$(git ls-files | rg '^packages/data/src/hooks/useMutation\.test\.ts$' | head -n 1)"
printf '%s\n' "FILE=$file"
wc -l "$file"
printf '%s\n' '--- focused test section ---'
cat -n "$file" | sed -n '1,150p'
printf '%s\n' '--- related fetch mock declarations/usages ---'
rg -n -C 3 'mockFetch|resolveFirst|resolveSecond|new Response|fetch' "$file" packages/data --glob '*.{ts,tsx,json}'
printf '%s\n' '--- TypeScript configurations ---'
git ls-files | rg '(^|/)(tsconfig(\..*)?\.json|package\.json)$' | sortRepository: Karanjot786/TermUI Length of output: 50374 🏁 Script executed: #!/bin/bash
set -eu
file="$(git ls-files | rg '^packages/data/src/hooks/useMutation\.test\.ts$' | head -n 1)"
printf '%s\n' "FILE=$file"
wc -l "$file"
cat -n "$file" | sed -n '1,150p'
rg -n -C 3 'mockFetch|resolveFirst|resolveSecond|new Response|fetch' "$file" packages/data --glob '*.{ts,tsx,json}'
git ls-files | rg '(^|/)(tsconfig(\..*)?\.json|package\.json)$' | sortRepository: Karanjot786/TermUI Length of output: 50374 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- useMutation implementation ---'
cat -n packages/data/src/hooks/useMutation.ts
printf '%s\n' '--- package and TypeScript configuration ---'
for f in $(git ls-files | rg '(^|/)(tsconfig(\..*)?\.json|package\.json)$' | sort); do
case "$f" in
packages/data/*|tsconfig*.json|package.json)
printf '%s\n' "--- $f ---"
cat "$f"
;;
esac
done
printf '%s\n' '--- available static tooling ---'
command -v node || true
command -v tsc || true
command -v npx || trueRepository: Karanjot786/TermUI Length of output: 7213 🏁 Script executed: #!/bin/bash
set -eu
probe="$(mktemp /tmp/response-shape-XXXXXX.ts)"
trap 'rm -f "$probe"' EXIT
cat > "$probe" <<'TS'
const partialResponse = {
ok: true,
status: 200,
text: async () => JSON.stringify({ value: 'new' }),
};
let resolveResponse!: (response: Response) => void;
resolveResponse(partialResponse);
const realResponse = new Response(JSON.stringify({ value: 'new' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
resolveResponse(realResponse);
TS
tsc --strict --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext --lib ES2022,DOM "$probe" 2>&1 || trueRepository: Karanjot786/TermUI Length of output: 393 Type the deferred response resolvers and use real Replace 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| mockFetch | ||
| .mockImplementationOnce(() => new Promise(resolve => { resolveFirst = resolve; })) | ||
| .mockImplementationOnce(() => new Promise(resolve => { resolveSecond = resolve; })); | ||
|
|
||
| render(createElement(TestComponent, { url: '/api/test' })); | ||
|
|
||
| const mutate = (global as any).hookResult.mutate; | ||
| const first = mutate({ value: 'old' }); | ||
| const second = mutate({ value: 'new' }); | ||
|
|
||
| resolveSecond({ | ||
| ok: true, | ||
| status: 200, | ||
| text: async () => JSON.stringify({ value: 'new' }), | ||
| }); | ||
| await second; | ||
| await flushPromises(); | ||
| expect((global as any).hookResult.data).toEqual({ value: 'new' }); | ||
|
|
||
| resolveFirst({ | ||
| ok: true, | ||
| status: 200, | ||
| text: async () => JSON.stringify({ value: 'old' }), | ||
| }); | ||
| await first; | ||
| await flushPromises(); | ||
|
|
||
| const result = (global as any).hookResult; | ||
| expect(result.data).toEqual({ value: 'new' }); | ||
| expect(result.mutationCount).toBe(1); | ||
| expect(result.loading).toBe(false); | ||
| }); | ||
|
|
||
| it('Error Handling on failed HTTP status', async () => { | ||
| // Setup the mock fetch to simulate a 404 error | ||
| mockFetch.mockResolvedValue({ | ||
|
|
@@ -127,4 +184,4 @@ describe('useMutation', () => { | |
| expect(result.data).toBeNull(); | ||
| expect(result.error).toBe(networkError); | ||
| }); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,34 @@ | ||
| // Mutation | ||
|
|
||
| import { useCallback, useState } from "@termuijs/jsx"; | ||
| import { useCallback, useRef, useState } from "@termuijs/jsx"; | ||
|
|
||
| export type HttpMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE' | ||
|
|
||
| export interface UseMutationReturn<T> { | ||
| mutate: (payload: unknown) => Promise<T>; | ||
| mutate: (payload: unknown) => Promise<T | null>; | ||
| reset: () => void; | ||
| data: T | null; | ||
| error: Error | null; | ||
| loading: boolean; | ||
| mutationCount: number; | ||
| } | ||
|
|
||
| async function readMutationResponse<T>(response: Response): Promise<T | null> { | ||
| if (response.status === 204 || response.status === 205) { | ||
| return null; | ||
| } | ||
|
|
||
| if (typeof response.text === 'function') { | ||
| const text = await response.text(); | ||
| if (text.trim() === '') return null; | ||
| return JSON.parse(text) as T; | ||
| } | ||
|
|
||
| return await response.json() as T; | ||
|
Comment on lines
+16
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate file outline ---'
ast-grep outline packages/data/src/hooks/useMutation.ts || true
printf '%s\n' '--- relevant source ---'
cat -n packages/data/src/hooks/useMutation.ts | sed -n '1,110p'
printf '%s\n' '--- TypeScript guidance and configuration ---'
rg -n --glob 'AGENTS.md' --glob 'tsconfig*.json' --glob '*.md' \
'type assertion|type assertions|strict|no-explicit-any|useMutation|readMutationResponse' . | head -200
printf '%s\n' '--- nearby assertion conventions ---'
rg -n --glob '*.ts' --glob '*.tsx' \
'JSON\.parse\(.*\)\s+as|response\.json\(\)\s+as|No type assertions|Response JSON' \
packages/data packages/core 2>/dev/null | head -200Repository: Karanjot786/TermUI Length of output: 6807 Document the unchecked response type assertions. Lines 24 and 27 cast unvalidated JSON to 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| } | ||
|
|
||
| /** | ||
| * useMutation — reactive HTTP mutation hook with loading and error states. | ||
| * useMutation - reactive HTTP mutation hook with loading and error states. | ||
| * | ||
| * Returns a `mutate` function that sends a request to the provided `url` | ||
| * with the specified HTTP `method` (default: POST). Updates are tracked via | ||
|
|
@@ -28,8 +42,10 @@ export function useMutation<T = unknown>(url: string, method: HttpMethod = 'POST | |
| const [error, setError] = useState<Error | null>(null) | ||
| const [data, setData] = useState<T | null>(null) | ||
| const [mutationCount, setMutationCount] = useState<number>(0) | ||
| const requestIdRef = useRef(0) | ||
|
|
||
| const mutate = useCallback(async (payload: unknown): Promise<T> => { | ||
| const mutate = useCallback(async (payload: unknown): Promise<T | null> => { | ||
| const requestId = ++requestIdRef.current | ||
|
Comment on lines
+45
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Invalidate pending mutations when If Proposed change const reset = useCallback(() => {
+ requestIdRef.current++;
setData(null)
setError(null)
setLoading(false)
}, [])🤖 Prompt for AI Agents |
||
| setLoading(true) | ||
| setError(null) | ||
|
|
||
|
|
@@ -46,17 +62,23 @@ export function useMutation<T = unknown>(url: string, method: HttpMethod = 'POST | |
| throw new Error(`HTTP error! status: ${response.status}`) | ||
| } | ||
|
|
||
| const result = (await response.json()) as T | ||
| setData(result) | ||
| setMutationCount((c)=> c+1) | ||
| const result = await readMutationResponse<T>(response) | ||
| if (requestId === requestIdRef.current) { | ||
| setData(result) | ||
| setMutationCount((c)=> c+1) | ||
| } | ||
| return result; | ||
|
|
||
| } catch (err) { | ||
| const errorObj = err instanceof Error ? err : new Error('Mutation failed'); | ||
| setError(errorObj); | ||
| if (requestId === requestIdRef.current) { | ||
| setError(errorObj); | ||
| } | ||
| throw errorObj; | ||
| } finally { | ||
| setLoading(false) | ||
| if (requestId === requestIdRef.current) { | ||
| setLoading(false) | ||
| } | ||
| } | ||
|
|
||
| }, [url, method]) | ||
|
|
@@ -68,4 +90,4 @@ export function useMutation<T = unknown>(url: string, method: HttpMethod = 'POST | |
| }, []) | ||
|
|
||
| return { mutate,reset, data, error, loading,mutationCount }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -100,6 +100,23 @@ describe('usePolling', () => { | |||||||||||||
| expect((global as any).hookResult.data).toBe(3); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| it('does not run immediately on dependency change while paused', async () => { | ||||||||||||||
| const fn = vi.fn().mockResolvedValue('data'); | ||||||||||||||
| const props = { fn, interval: 1000, deps: [1] as unknown[] }; | ||||||||||||||
| const { rerender } = render(createElement(TestComponent, props)); | ||||||||||||||
|
|
||||||||||||||
| await flushPromises(); | ||||||||||||||
| expect(fn).toHaveBeenCalledTimes(1); | ||||||||||||||
|
|
||||||||||||||
| (global as any).hookResult.pause(); | ||||||||||||||
| props.deps = [2]; | ||||||||||||||
| rerender(); | ||||||||||||||
|
Comment on lines
+111
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 8 'resolvedProps|createElement' packages/jsx/src/createElement.ts
rg -n -C 12 'rerender' packages/testing/src/render.tsRepository: Karanjot786/TermUI Length of output: 4230 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- test ---'
sed -n '1,180p' packages/data/src/hooks/usePolling.test.ts
printf '%s\n' '--- re-render implementation and prop handling ---'
rg -n -C 14 'function reRenderComponent|const reRenderComponent|reRenderComponent|storedProps|props' packages/jsx/src packages/testing/src/render.tsRepository: Karanjot786/TermUI Length of output: 50377 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- polling hook ---'
sed -n '1,260p' packages/data/src/hooks/usePolling.ts
printf '%s\n' '--- reconciler declarations and implementation ---'
rg -n -A 80 -B 12 'export function reRenderComponent|function reRenderComponent|interface ComponentInstance|type ComponentInstance|props:' packages/jsx/src/reconciler.tsRepository: Karanjot786/TermUI Length of output: 23035 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- effect dependency comparison and state scheduling ---'
rg -n -A 90 -B 15 'export function useEffect|function useEffect|setState|scheduleRender|deps|Object.is' packages/jsx/src/hooks.ts
printf '%s\n' '--- component instance creation and prop updates ---'
sed -n '480,575p' packages/jsx/src/reconciler.ts
sed -n '640,710p' packages/jsx/src/reconciler.tsRepository: Karanjot786/TermUI Length of output: 26494 🏁 Script executed: #!/bin/bash
set -euo pipefail
node - <<'JS'
const input = { fn: 'fn', interval: 1000, deps: [1] };
const storedProps = { ...input };
const originalDeps = input.deps;
input.deps = [2];
if (storedProps.deps[0] !== 1) {
throw new Error('Outer-prop reassignment unexpectedly changed stored props');
}
originalDeps[0] = 2;
if (storedProps.deps[0] !== 2) {
throw new Error('Nested dependency mutation did not reach stored props');
}
const previousEffectDeps = [1000, undefined, false, 1];
const nextEffectDeps = [1000, undefined, true, ...storedProps.deps];
const changed = nextEffectDeps.some((value, index) =>
!Object.is(value, previousEffectDeps[index]),
);
if (!changed || nextEffectDeps[3] !== 2) {
throw new Error('Dependency mutation did not reach the effect dependency list');
}
console.log('outer reassignment is isolated; nested mutation reaches usePolling');
JSRepository: Karanjot786/TermUI Length of output: 223 Make the dependency change reach the rendered component.
Keep the original array and update its element before Proposed test fix- const props = { fn, interval: 1000, deps: [1] as unknown[] };
+ const deps = [1];
+ const props = { fn, interval: 1000, deps };
...
- props.deps = [2];
+ deps[0] = 2;
rerender();📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| await flushPromises(); | ||||||||||||||
|
|
||||||||||||||
| expect(fn).toHaveBeenCalledTimes(1); | ||||||||||||||
| expect((global as any).hookResult.paused).toBe(true); | ||||||||||||||
|
Comment on lines
+103
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Remove unannotated type assertions from the added test. The added lines use 🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| it('Skips interval tick while a request is still in flight', async () => { | ||||||||||||||
| let resolveFirst!: (value: string) => void; | ||||||||||||||
| const fn = vi.fn().mockImplementationOnce(() => new Promise<string>(resolve => { | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -39,8 +39,11 @@ export function usePolling<T>( | |||||||||||||||||||||||||||||||||||||
| const requestIdRef = useRef(0); | ||||||||||||||||||||||||||||||||||||||
| const adaptiveRef = useRef<AdaptivePollingController | null>(null); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const execute = async () => { | ||||||||||||||||||||||||||||||||||||||
| const execute = async (force = false) => { | ||||||||||||||||||||||||||||||||||||||
| const startedAt = Date.now(); | ||||||||||||||||||||||||||||||||||||||
| if (!force && pausedRef.current) { | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| if (inFlightRef.current) { | ||||||||||||||||||||||||||||||||||||||
| adaptiveRef.current?.begin(); | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -89,15 +92,17 @@ export function usePolling<T>( | |||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const refresh = () => { | ||||||||||||||||||||||||||||||||||||||
| execute(); | ||||||||||||||||||||||||||||||||||||||
| execute(true); | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||
| mountedRef.current = true; | ||||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||||
| adaptiveRef.current = options.adaptive ? new AdaptivePollingController(options.adaptive) : null; | ||||||||||||||||||||||||||||||||||||||
| execute(); | ||||||||||||||||||||||||||||||||||||||
| if (!paused) { | ||||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||||
| execute(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
99
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Clear If Set Proposed fix- if (!paused) {
+ if (paused) {
+ setLoading(false);
+ } else {
setLoading(true);
execute();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 8 'const tick = async|mountedRef|adaptiveRef' packages/data/src/hooks/usePolling.ts
rg -n -C 8 'export function useEffect' packages/jsx/src/hooks.tsRepository: Karanjot786/TermUI Length of output: 4638 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- usePolling outline ---'
ast-grep outline packages/data/src/hooks/usePolling.ts --view expanded
printf '%s\n' '--- usePolling source ---'
cat -n packages/data/src/hooks/usePolling.ts
printf '%s\n' '--- AdaptivePollingController references ---'
rg -n -C 10 'class AdaptivePollingController|AdaptivePollingController|nextDelay|success\(|failure\(|begin\(' packages/data/srcRepository: Karanjot786/TermUI Length of output: 23491 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- JSX effect implementation and cleanup scheduling ---'
ast-grep outline packages/jsx/src/hooks.ts --view expanded
rg -n -C 18 'function useEffect|export function useEffect|cleanup|effects' packages/jsx/src/hooks.ts packages/jsx/src
printf '%s\n' '--- polling-related tests and references ---'
fd -i 'poll' packages
rg -n -C 12 'usePolling|pause\(\)|resume\(\)|adaptive' packages/data packages/jsx --glob '*.{ts,tsx}'Repository: Karanjot786/TermUI Length of output: 50377 🏁 Script executed: #!/bin/bash
set -euo pipefail
node - <<'JS'
const events = [];
const timers = [];
let mounted = true;
let adaptive = { name: 'old' };
let oldTimeout;
let newTimeout;
function setTimeoutModel(callback, owner) {
const timer = { callback, owner, cleared: false };
timers.push(timer);
return timer;
}
function clearTimeoutModel(timer) {
if (timer) timer.cleared = true;
}
function executeModel() {
return Promise.resolve().then(() => {
events.push('old execute resumed');
return Promise.resolve();
});
}
async function oldTick() {
await executeModel();
if (mounted && adaptive) {
oldTimeout = setTimeoutModel(oldTick, adaptive.name);
events.push(`old tick scheduled ${adaptive.name}`);
}
}
oldTimeout = setTimeoutModel(oldTick, 'old');
oldTimeout.cleared = false;
const oldTimer = timers.shift();
oldTimer.callback();
mounted = false;
clearTimeoutModel(oldTimeout);
adaptive = { name: 'new' };
mounted = true;
newTimeout = setTimeoutModel(() => {}, 'new');
setImmediate(() => {
const orphaned = timers.filter(timer => !timer.cleared);
console.log(JSON.stringify({
events,
scheduledOwners: orphaned.map(timer => timer.owner),
oldCleanupClearedOldTimer: oldTimeout?.cleared ?? false,
newCleanupTimerOwner: newTimeout.owner,
}, null, 2));
});
JSRepository: Karanjot786/TermUI Length of output: 354 Guard adaptive polling callbacks by effect generation. When Capture the controller per effect or request, and check an effect-local 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if (adaptiveRef.current) { | ||||||||||||||||||||||||||||||||||||||
| let timeout: ReturnType<typeof setTimeout> | undefined; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -131,7 +136,7 @@ export function usePolling<T>( | |||||||||||||||||||||||||||||||||||||
| clearInterval(timer); | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||||||||||||||||||||||||||||||||||||||
| }, [interval, options.adaptive, ...deps]); | ||||||||||||||||||||||||||||||||||||||
| }, [interval, options.adaptive, paused, ...deps]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| return { data, error, loading, paused, pause, resume, refresh }; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,9 @@ export function useSSE<T = string>( | |
|
|
||
| useEffect(() => { | ||
| let isMounted = true; | ||
| setData(null); | ||
| setError(null); | ||
| setLoading(true); | ||
|
Comment on lines
+31
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 4 '\buseSSE\s*\(' packages --glob '*.ts' --glob '*.tsx' || trueRepository: Karanjot786/TermUI Length of output: 3827 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- useSSE implementation ---'
cat -n packages/data/src/hooks/useSSE.ts
echo '--- useSSE tests ---'
cat -n packages/data/src/hooks/useSSE.test.ts
echo '--- hook exports and related hook usage ---'
rg -n -C 3 'useSSE|useEffect|useState|EventSource' packages/data/src packages/data/package.json --glob '*.ts' --glob '*.tsx' --glob 'package.json'Repository: Karanjot786/TermUI Length of output: 46767 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- JSX hook implementation files ---'
fd -i 'hooks|fiber|effect|state' packages/jsx/src packages/core/src 2>/dev/null | head -80
echo '--- useEffect and useState definitions/usages ---'
rg -n -C 8 'export function (useEffect|useState)|function (useEffect|useState)|const useEffect|const useState|run.*effect|effect.*deps|hookIndex' packages/jsx/src packages/core/src --glob '*.ts' --glob '*.tsx'Repository: Karanjot786/TermUI Length of output: 43570 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- state, effect, and render scheduling implementation ---'
sed -n '200,330p' packages/jsx/src/hooks.ts
rg -n -C 12 'scheduleRender|runEffects|\.effects|cleanup|renderComponent|setCurrentFiber' packages/jsx/src packages --glob '*.ts' --glob '*.tsx' | head -260Repository: Karanjot786/TermUI Length of output: 21191 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- scheduleRender ---'
rg -n -C 20 'function scheduleRender|export function scheduleRender|_requestRender|setRequestRender' packages/jsx/src/hooks.ts packages/jsx/src --glob '*.ts' --glob '*.tsx'
echo '--- effect execution ---'
rg -n -C 20 'ran = true|record\.ran|runEffects|runLayoutEffects|effects\.filter|effects\.forEach|cleanups' packages/jsx/src --glob '*.ts' --glob '*.tsx'Repository: Karanjot786/TermUI Length of output: 50377 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '--- runEffects implementation ---'
rg -n -C 30 'export function runEffects|function runEffects' packages/jsx/src/hooks.ts
echo '--- component rerender implementation ---'
rg -n -C 35 'export function reRenderComponent|function reRenderComponent' packages/jsx/src/reconciler.tsRepository: Karanjot786/TermUI Length of output: 5193 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '620,690p' packages/jsx/src/reconciler.ts
rg -n -C 18 'runEffects\(' packages/jsx/src/reconciler.ts packages/jsx/src/render.tsRepository: Karanjot786/TermUI Length of output: 11503 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from dataclasses import dataclass
`@dataclass`
class Effect:
deps: tuple
ran: bool = False
cleanup_count: int = 0
class Model:
def __init__(self):
self.data = None
self.error = None
self.loading = True
self.effect = None
self.sources = 0
self.closed = 0
self.pending_render = False
def set_state(self, name, value):
if getattr(self, name) != value:
setattr(self, name, value)
self.pending_render = True
def render(self, parser_identity):
deps = ("url", parser_identity)
if self.effect is None:
self.effect = Effect(deps)
elif self.effect.deps != deps:
self.effect.cleanup_count += 1
self.effect.deps = deps
self.effect.ran = False
if not self.effect.ran:
self.set_state("data", None)
self.set_state("error", None)
self.set_state("loading", True)
self.sources += 1
self.effect.ran = True
def message(self, value):
self.set_state("data", value)
self.set_state("error", None)
self.set_state("loading", False)
# The real hook schedules a render when state changes. The component
# creates a fresh inline parser on every render.
def flush(self, parser_identity):
if self.pending_render:
self.pending_render = False
self.render(parser_identity)
m = Model()
m.render(parser_identity=1) # mount
m.message("event-1")
m.flush(parser_identity=2) # render caused by setData
m.flush(parser_identity=3) # render caused by effect reset
print({
"event_data_after_flushes": m.data,
"event_sources_opened": m.sources,
"old_sources_closed": m.effect.cleanup_count,
"effect_cleanup_runs": m.effect.cleanup_count,
})
assert m.sources == 3
assert m.effect.cleanup_count == 2
assert m.data is None
PYRepository: Karanjot786/TermUI Length of output: 269 Store the latest parser in a ref or require 🤖 Prompt for AI Agents |
||
|
|
||
| if (typeof EventSource === 'undefined') { | ||
| setError(new Error('EventSource is not supported in this environment')); | ||
|
|
@@ -61,7 +64,7 @@ export function useSSE<T = string>( | |
| isMounted = false; | ||
| source.close(); | ||
| }; | ||
| }, [url]); | ||
| }, [url, parse]); | ||
|
|
||
| return { data, error, loading }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: Karanjot786/TermUI
Length of output: 50375
🏁 Script executed:
Repository: Karanjot786/TermUI
Length of output: 12760
🏁 Script executed:
Repository: Karanjot786/TermUI
Length of output: 4510
🌐 Web query:
Karanjot786 TermUI packages/adapters/src/execa/index.ts ExecaFunction💡 Result:
The file path packages/adapters/src/execa/index.ts is located within the TermUI project, an open-source TypeScript/JavaScript framework for building terminal applications maintained by Karanjot Singh (
@Karanjot786) [1][2]. In the context of this repository, the code within packages/adapters/src/execa/index.ts is used to create an abstraction or wrapper around the popular Node.js library execa (which handles process execution) [3][4][5]. The term ExecaFunction refers to a specific type or exported function within that adapter layer, designed to standardize or simplify how child processes are executed or managed across the TermUI framework's various packages [3][4]. As of the latest releases (including v0.1.7), this adapter file has undergone minor maintenance updates [6][4]. It is not a standard part of the upstream execa library, but rather a custom integration within the TermUI ecosystem [1][7][5].Citations:
🏁 Script executed:
Repository: Karanjot786/TermUI
Length of output: 13087
🌐 Web query:
Execa 8 TypeScript types ExecaChildPromise ExecaFunction return type💡 Result:
In Execa 8, the TypeScript types and return values for asynchronous methods function as follows: The return value of all asynchronous Execa methods (such as execa, execaCommand, etc.) is an object that acts as both a Promise and a Node.js ChildProcess [1][2]. Key TypeScript types in Execa 8: 1. ExecaChildProcess: In versions prior to the breaking changes introduced in version 9, the return type of asynchronous Execa methods is represented by the ExecaChildProcess type [3][4]. This type extends the standard Node.js ChildProcess interface, adds Execa-specific methods (like cancel and an enhanced kill), and acts as a Promise that resolves to the process result [3][4]. 2. ExecaChildPromise: This type represents the Execa-specific promise-like methods added to the subprocess, including catch, kill, and cancel [3][4]. Transitioning to Execa 9: It is important to note that these types were renamed in the major release of Execa 9 [5][6]. If you are migrating or working with newer codebases: - The ExecaChildProcess type was renamed to ResultPromise [7][5]. - The ExecaChildPromise type was renamed to Subprocess [7][5]. Because types in Execa are designed to be automatically inferred, you generally do not need to explicitly type the return value of execa calls [8][9]. Explicit type annotation is primarily necessary only when defining custom functions that handle these objects as parameters or return values [8][9]. For further details on the current API, refer to the official Execa documentation [10][11].
Citations:
🏁 Script executed:
Repository: Karanjot786/TermUI
Length of output: 8029
Use Execa’s callable type for
ExecaFunction.Define it as
typeof import('execa').execainstead of a custom function returningany. This preserves Execa’s overloads and child-process return type.🤖 Prompt for AI Agents
Source: Coding guidelines