UNPKG

@gensx/react

Version:

React hooks and components for GenSX AI workflows.

173 lines (154 loc) 5.36 kB
import type { JsonValue, ObjectMessage, Operation, WorkflowMessage, } from "@gensx/core"; import * as fastJsonPatch from "fast-json-patch"; import { useEffect, useRef } from "react"; import { useState } from "react"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type PublishableData = any; // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters export function useObject<T = JsonValue>( events: WorkflowMessage[], label: string, ): T | undefined { const [result, setResult] = useState<T | undefined>(undefined); // Store the reconstructed object and last processed event index const reconstructedRef = useRef<JsonValue>({}); const lastIndexRef = useRef<number>(-1); const lastEventsRef = useRef<WorkflowMessage[]>([]); useEffect(() => { // Find all relevant object events const objectEvents = events.filter( (event): event is ObjectMessage => event.type === "object" && event.label === label, ); // Detect reset: events array replaced or truncated const isReset = events !== lastEventsRef.current || objectEvents.length < lastIndexRef.current + 1; if (isReset) { reconstructedRef.current = {}; lastIndexRef.current = -1; } // Apply only new patches for (let i = lastIndexRef.current + 1; i < objectEvents.length; i++) { const event = objectEvents[i]; if (event.isInitial) { reconstructedRef.current = {}; } try { reconstructedRef.current = applyObjectPatches( event.patches, reconstructedRef.current, ); } catch (error) { console.warn(`Failed to apply patches for object "${label}":`, error); } } lastIndexRef.current = objectEvents.length - 1; lastEventsRef.current = events; setResult( objectEvents.length > 0 ? (reconstructedRef.current as T) : undefined, ); }, [events, label]); return result; } /** * Apply a JSON patch to reconstruct object state. This is useful for consumers who want to reconstruct the full object state from patches. * * @param patches - The JSON patch operations to apply. * @param currentState - The current state of the object (defaults to empty object). * @returns The new state after applying the patches. */ function applyObjectPatches( patches: Operation[], currentState: PublishableData = {}, ): PublishableData { let document = fastJsonPatch.deepClone(currentState); let standardPatches: fastJsonPatch.Operation[] = []; for (const operation of patches) { if (operation.op === "string-append") { // Handle string append operation if (operation.path === "") { // Root-level string append if (typeof document === "string") { document = document + operation.value; } else { // Warn and skip instead of throwing or replacing console.warn( `Cannot apply string-append: root value is not a string. Skipping operation.`, ); // Do nothing } continue; } const pathParts = operation.path.split("/").slice(1); // Remove empty first element const target = getValueByPath(document, pathParts.slice(0, -1)); const property = pathParts[pathParts.length - 1]; if (typeof target === "object" && target !== null) { const currentValue = (target as Record<string, JsonValue>)[property]; if (typeof currentValue === "string") { (target as Record<string, JsonValue>)[property] = currentValue + operation.value; } else { // Warn and skip instead of replacing console.warn( `Cannot apply string-append: target path '${operation.path}' is not a string. Skipping operation.`, ); // Do nothing } } else { // Warn and skip instead of throwing console.warn( `Cannot apply string-append: target path '${operation.path}' does not exist or is not an object. Skipping operation.`, ); // Do nothing } } else { // Handle standard JSON Patch operations standardPatches.push(operation); } } if (standardPatches.length > 0) { const result = fastJsonPatch.applyPatch( document, fastJsonPatch.deepClone(standardPatches) as fastJsonPatch.Operation[], ); return result.newDocument; } return document; } /** * Helper function to get a value by path in an object or array (RFC 6901 compliant) */ function getValueByPath(obj: PublishableData, path: string[]): PublishableData { let current: PublishableData = obj; for (const segment of path) { if (Array.isArray(current)) { const idx = Number(segment); if (!Number.isNaN(idx) && idx >= 0 && idx < current.length) { current = current[idx]; } else { return undefined; } } else if (isPlainObject(current)) { if (segment in current) { current = current[segment]; } else { return undefined; } } else { return undefined; } } return current; } /** * Utility to check if a value is a non-null, non-array object */ function isPlainObject(val: unknown): val is Record<string, JsonValue> { return typeof val === "object" && val !== null && !Array.isArray(val); }