-
Notifications
You must be signed in to change notification settings - Fork 0
web: StrictMode-safe runtime lifecycle + progressive rendering + state dedupe #3
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: ragnorc/dac-to-colombo-port
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 |
|---|---|---|
|
|
@@ -48,6 +48,13 @@ export interface ReadRequest { | |
|
|
||
| export interface ReadOutput { | ||
| query_name: string; | ||
| /** | ||
| * The branch (or snapshot id) the read resolved against, normalized to a | ||
| * single label. The omnigraph server returns a `{branch, snapshot}` object; | ||
| * the client facade collapses it to this string because nothing in colombo | ||
| * distinguishes the two — it's display metadata only. Keep it a string | ||
| * unless/until a consumer needs the structured form. | ||
| */ | ||
| target: string; | ||
| row_count: number; | ||
| columns: string[]; | ||
|
|
@@ -234,11 +241,22 @@ class NotebookRuntimeImpl implements NotebookRuntime { | |
|
|
||
| applyStateChanges(changes: RuntimeStateChange[]): void { | ||
| if (this.disposed || changes.length === 0) return; | ||
| // Drop no-op changes: a bound control (e.g. the web Select) can re-emit its | ||
| // current value on every render, which would otherwise re-run dependent | ||
| // cells in an endless loop. Only proceed when something actually changed. | ||
| const effective = changes.filter( | ||
| (change) => | ||
| !valuesEqual( | ||
| resolveStatePointer(this.snapshot.state, change.path), | ||
| change.value, | ||
| ), | ||
| ); | ||
|
Comment on lines
+247
to
+253
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.
When a single Useful? React with 👍 / 👎. |
||
| if (effective.length === 0) return; | ||
| let next = this.snapshot.state; | ||
| for (const change of changes) { | ||
| for (const change of effective) { | ||
| next = setAtPointer(next, change.path, change.value); | ||
| } | ||
| const changedPaths = changes.map((change) => change.path); | ||
| const changedPaths = effective.map((change) => change.path); | ||
| this.snapshot = { | ||
| ...this.snapshot, | ||
| state: next, | ||
|
|
@@ -874,3 +892,11 @@ function errorMessage(err: unknown): string { | |
| function isAbortError(err: unknown): boolean { | ||
| return err instanceof Error && err.name === "AbortError"; | ||
| } | ||
|
|
||
| function valuesEqual(a: unknown, b: unknown): boolean { | ||
| if (a === b) return true; | ||
| if (a !== null && b !== null && typeof a === "object" && typeof b === "object") { | ||
| return JSON.stringify(a) === JSON.stringify(b); | ||
| } | ||
| return false; | ||
| } | ||
|
Comment on lines
+896
to
+902
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.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | ||
| import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; | ||
| import { JSONUIProvider, Renderer } from "@json-render/react"; | ||
| import { | ||
| createNotebookRuntime, | ||
| type CellExecution, | ||
| type NotebookRuntime, | ||
| type RuntimeSnapshot, | ||
| } from "@omnigraph/runtime"; | ||
|
|
||
|
|
@@ -60,42 +61,51 @@ export function App(): React.ReactElement { | |
| } | ||
|
|
||
| function RuntimeApp({ config }: { config: AppConfig }): React.ReactElement { | ||
| const [runtime] = useState(() => | ||
| createNotebookRuntime({ notebook: config.notebook, source: config.source }), | ||
| ); | ||
| const [snapshot, setSnapshot] = useState<RuntimeSnapshot>(() => | ||
| runtime.getSnapshot(), | ||
| ); | ||
| const runtimeRef = useRef<NotebookRuntime | null>(null); | ||
| const [snapshot, setSnapshot] = useState<RuntimeSnapshot | null>(null); | ||
| const [dismissedMutationError, setDismissedMutationError] = | ||
| useState<string | null>(null); | ||
|
|
||
| // Create the runtime *inside* the effect (not useState) so React StrictMode's | ||
| // dev mount → cleanup → mount cycle disposes the first runtime and builds a | ||
| // fresh one — rather than re-subscribing to a disposed runtime that never | ||
| // notifies (which leaves the page stuck on the loading skeleton). | ||
| useEffect(() => { | ||
| const runtime = createNotebookRuntime({ | ||
| notebook: config.notebook, | ||
| source: config.source, | ||
| }); | ||
| runtimeRef.current = runtime; | ||
| setSnapshot(runtime.getSnapshot()); | ||
| const unsubscribe = runtime.subscribe(() => | ||
| setSnapshot(runtime.getSnapshot()), | ||
| ); | ||
| return () => { | ||
| unsubscribe(); | ||
| runtime.dispose(); | ||
| runtimeRef.current = null; | ||
| }; | ||
| }, [config]); | ||
|
|
||
| const handleStateChange = useCallback( | ||
| (changes: Array<{ path: string; value: unknown }>) => { | ||
| runtime.applyStateChanges(changes); | ||
| runtimeRef.current?.applyStateChanges(changes); | ||
| }, | ||
| [runtime], | ||
| [], | ||
| ); | ||
|
|
||
| const handlers = useMemo( | ||
| () => ({ | ||
| mutate: async (params: Record<string, unknown>) => { | ||
| setDismissedMutationError(null); | ||
| await runtime.dispatch("mutate", { params }); | ||
| await runtimeRef.current?.dispatch("mutate", { params }); | ||
| }, | ||
| }), | ||
| [runtime], | ||
| [], | ||
| ); | ||
|
Comment on lines
97
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.
The empty dependency arrays are correct because Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
|
|
||
| useEffect(() => { | ||
| const unsubscribe = runtime.subscribe(() => setSnapshot(runtime.getSnapshot())); | ||
| return () => { | ||
| unsubscribe(); | ||
| runtime.dispose(); | ||
| }; | ||
| }, [runtime]); | ||
|
|
||
| const mutationError: ClassifiedError | null = | ||
| snapshot.mutationError !== null && | ||
| snapshot?.mutationError != null && | ||
| snapshot.mutationError !== dismissedMutationError | ||
| ? classifyMutationError(snapshot.mutationError) | ||
| : null; | ||
|
|
@@ -125,18 +135,16 @@ function RuntimeApp({ config }: { config: AppConfig }): React.ReactElement { | |
| </span> | ||
| </header> | ||
|
|
||
| {snapshot.status === "loading" && ( | ||
| <LoadingSkeleton | ||
| cellTitles={config.notebook.cells.map((c) => c.id)} | ||
| /> | ||
| )} | ||
| {snapshot.status === "fatal" && ( | ||
| {snapshot === null ? ( | ||
| <LoadingSkeleton cellTitles={config.notebook.cells.map((c) => c.id)} /> | ||
| ) : snapshot.status === "fatal" ? ( | ||
| <FatalPanel | ||
| title="Failed to run notebook" | ||
| message={snapshot.error ?? "runtime failed"} | ||
| /> | ||
| )} | ||
| {snapshot.status === "ready" && ( | ||
| ) : ( | ||
| // Render every cell progressively — each shows its own loading / | ||
| // data / error state, so a slow or failed cell never blanks the page. | ||
| <div className="space-y-6"> | ||
| {snapshot.cells.map((cell) => ( | ||
| <CellCard key={cell.cell.id} cell={cell} /> | ||
|
|
@@ -206,6 +214,13 @@ function CellCard({ cell }: { cell: CellExecution }): React.ReactElement { | |
| {cell.error === null && cell.spec !== null && ( | ||
| <Renderer spec={cell.spec} registry={webRegistry} /> | ||
| )} | ||
| {cell.error === null && cell.spec === null && !isControl && ( | ||
| <div className="space-y-2"> | ||
| <SkeletonBar w="w-full" /> | ||
| <SkeletonBar w="w-5/6" /> | ||
| <SkeletonBar w="w-2/3" /> | ||
| </div> | ||
| )} | ||
| </section> | ||
| ); | ||
| } | ||
|
|
||
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.
Batch no-op filter wrong
Low Severity
In
applyStateChanges, each incoming change is compared to the pre-batch snapshot state when deciding if it is a no-op. A later entry for the same JSON pointer that matches the original value can be dropped even though an earlier entry in the same batch already changed that pointer, so the final state may not match applying all patches in order.Reviewed by Cursor Bugbot for commit 6a21af4. Configure here.