Skip to content

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

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

charliecreates[bot]
Copy link
Contributor

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:

    • Adds src/hooks/useStreamingJsonHook.ts exporting useStreamingJsonHook, which takes an array of expected key strings and an optional configuration.
  • Types & Initial State:

    • Defines KeyState, KeyMeta, and initializes props, meta, and isStreamDone as per spec.
  • Streaming Logic:

    • Handles streams of { key, value } tokens and a terminal done signal.
    • On first token for a key, updates state to streaming and timestamps. Transitions previous streaming key to complete on a new key.
    • On done, finalizes all keys' states appropriately (complete or skipped), and records completion timestamps.
    • Ignores or throws on unknown keys according to config.
  • Return Value:

    • Returns an object { props, meta, isStreamDone } implementing StreamingJsonResult.
  • Edge Cases:

    • Properly handles unknown keys and cases without a done signal.
  • Unit Tests:

    • Adds comprehensive unit tests in tests/hooks/useStreamingJsonHook.test.ts for these scenarios:
      1. All keys streamed, then done.
      2. Some expected keys never streamed.
      3. Streaming without done.
      4. Malformed input handling (unknown keys, etc.).

This work does not modify unrelated code and introduces the feature as a fully independent, well-tested module.

Resolves TAM-153.

…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
@charliecreates charliecreates bot requested a review from CharlieHelps May 23, 2025 17:37
Copy link

vercel bot commented May 23, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
showcase ✅ Ready (Inspect) Visit Preview 💬 Add feedback May 23, 2025 6:16pm

Copy link
Contributor Author

@charliecreates charliecreates bot left a 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 (notStartedstreamingcomplete / skipped).
    • Handles unknown keys via configurable onUnknownKey option.
    • Exposes processToken and markDone utilities.
  • Unit tests added in __tests__/use-streaming-json-hook.test.ts covering normal flow, skipped keys, unknown keys, and no-done scenarios.

Comment on lines 92 to 104
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;
}
Copy link
Contributor Author

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.

Comment on lines 148 to 175
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;
}, []);
Copy link
Contributor Author

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.

@charliecreates charliecreates bot removed the request for review from CharlieHelps May 23, 2025 17:39
Copy link
Collaborator

@CharlieHelps fix the issues identified and also rename the hook to useStreamProps

Copy link
Contributor Author

charliecreates bot commented May 23, 2025

ASCII only, unified comment style.

• Replaced the lingering em-dash in the test with a plain hyphen.
• Switched the brief JSDoc block to a single-line comment in useStreamProps.
• Updated dashes in several other comment lines to ASCII.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants