-
Notifications
You must be signed in to change notification settings - Fork 28
feat(hooks): implement useStreamingJsonHook with per-key state tracking (TAM-153) #452
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?
feat(hooks): implement useStreamingJsonHook with per-key state tracking (TAM-153) #452
Conversation
…tate tracking (TAM-153) Introduce a new React hook, useStreamingJsonHook, that incrementally builds a props object from a token stream while tracking detailed per-key streaming state. This includes full lifecycle signalling for each key: "notStarted", "streaming", "complete", and "skipped", as described in Linear issue TAM-153. The hook supports handling unknown keys with a configurable mode ("ignore" or "throw") and exposes utilities for injecting new tokens and marking the stream as complete. Accompanying tests cover: - Normal streaming flows, including key transitions - Proper marking of skipped and completed states - Unknown key handling (default "ignore" and "throw" mode) - Completion signalling and streaming state maintenance
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
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.
Hook works as specified, but two idempotency guards are missing: processToken
can still mutate state after markDone
, and markDone
itself can be called multiple times without protection. Adding early-return checks will harden the public API against accidental misuse.
Summary of changes
What changed in this PR
- New hook
useStreamingJsonHook
(src/hooks/use-streaming-json-hook.ts
)- Tracks per-key streaming lifecycle (
notStarted
➜streaming
➜complete
/skipped
). - Handles unknown keys via configurable
onUnknownKey
option. - Exposes
processToken
andmarkDone
utilities.
- Tracks per-key streaming lifecycle (
- Unit tests added in
__tests__/use-streaming-json-hook.test.ts
covering normal flow, skipped keys, unknown keys, and no-done scenarios.
const processToken = useCallback( | ||
({ key, value }: StreamToken) => { | ||
if (!expectedKeySet.has(key)) { | ||
if (onUnknownKey === "throw") { | ||
throw new Error( | ||
`Received token for unexpected key "${key}" (expected one of: ${[ | ||
...expectedKeySet, | ||
].join(", ")})`, | ||
); | ||
} | ||
// Unknown key in "ignore" mode – drop silently. | ||
return; | ||
} |
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.
processToken
will happily mutate state even after the consumer has called markDone()
. Once isStreamDone
is true
, further tokens should either be ignored or produce an explicit error to prevent accidental state corruption.
Suggestion
if (isStreamDone) {
// Stream already closed – ignore further tokens or consider throwing.
return;
}
Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this guard.
const markDone = useCallback(() => { | ||
const now = Date.now(); | ||
|
||
setMeta((prev) => { | ||
const next: Record<string, KeyMeta> = {}; | ||
for (const key of Object.keys(prev)) { | ||
const entry = prev[key]; | ||
if (entry.state === "streaming") { | ||
next[key] = { | ||
...entry, | ||
state: "complete", | ||
streamCompletedAt: now, | ||
}; | ||
} else if (entry.state === "notStarted") { | ||
next[key] = { | ||
...entry, | ||
state: "skipped", | ||
}; | ||
} else { | ||
next[key] = entry; | ||
} | ||
} | ||
return next; | ||
}); | ||
|
||
setIsStreamDone(true); | ||
activeKeyRef.current = null; | ||
}, []); |
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.
markDone
is not idempotent—calling it twice will reset timestamps and, in some edge cases, flip a key from skipped
back to complete
. Making the function a no-op when the stream is already marked done improves robustness and makes the API safer for repeat calls (e.g. when a done
signal is fired twice by mistake).
Suggestion
if (isStreamDone) return; // Already finalised – be idempotent
Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
@CharlieHelps fix the issues identified and also rename the hook to useStreamProps |
ASCII only, unified comment style. • Replaced the lingering em-dash in the test with a plain hyphen. All code now meets style and ASCII constraints. |
…dempotency guards, update tests Renames the hook from `useStreamingJsonHook` to `useStreamProps` for improved clarity and consistency. Adds idempotency guards to prevent multiple invocations of `markDone` or `processToken` after the stream is finished, ensuring that state updates are safe and predictable. Updates all relevant tests to use the new hook name and verifies idempotency behavior. Removes the old file and migrates all usage to the new naming. This refactor improves maintainability and prevents unintended state changes after completing the stream.
Remove the duplicate test file use-stream-props.test.ts, which overlapped with use-streaming-json-hook.test.ts. Fix non-ASCII dashes in comments and clarify a comment for consistency. This cleanup prevents redundant test runs and ensures uniform, ASCII-only source in test descriptions.
The top-level meta-comment indicating updated tests was removed from useStreamingJsonHook test file. This comment was no longer relevant and did not add value to the test documentation or maintenance. No functional changes were introduced.
Replaced all instances of the non-ASCII dash (—) with the standard ASCII dash (-) for consistency and ASCII compliance. Also standardized comment styles related to separators and descriptions to improve readability and maintain coding conventions across test and implementation files. No logic changes.
This PR introduces the
useStreamingJsonHook
as specified in TAM-153, providing a streaming JSON hook with detailed per-key state tracking and timestamps. The implementation includes:New Hook Module:
src/hooks/useStreamingJsonHook.ts
exportinguseStreamingJsonHook
, which takes an array of expected key strings and an optional configuration.Types & Initial State:
KeyState
,KeyMeta
, and initializesprops
,meta
, andisStreamDone
as per spec.Streaming Logic:
{ key, value }
tokens and a terminaldone
signal.streaming
and timestamps. Transitions previous streaming key tocomplete
on a new key.done
, finalizes all keys' states appropriately (complete
orskipped
), and records completion timestamps.Return Value:
{ props, meta, isStreamDone }
implementingStreamingJsonResult
.Edge Cases:
done
signal.Unit Tests:
tests/hooks/useStreamingJsonHook.test.ts
for these scenarios:done
.done
.This work does not modify unrelated code and introduces the feature as a fully independent, well-tested module.
Resolves TAM-153.