Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions packages/core/src/store/actions/node-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,26 +459,43 @@ function formatNumericValue(value: number) {
return String(value)
}

function numericSanitizeIssuesToMessage(issues: NumericSanitizeIssue[]): string {
return issues
.map((issue) => {
const path = issue.path.map(String).join('.') || '<root>'
const to = issue.to === undefined ? '' : ` -> ${formatNumericValue(issue.to)}`
return `${path}: ${formatNumericValue(issue.from)} ${issue.action}${to}`
})
.join('; ')
export function numericSanitizeIssuesToMessage(
issues: NumericSanitizeIssue[] | null | undefined,
): string {
if (!Array.isArray(issues)) return ''

try {
return issues
.map((issue) => {
const path = Array.isArray(issue?.path)
? issue.path.map(String).join('.') || '<root>'
: '<unknown>'
const to = issue?.to === undefined ? '' : ` -> ${formatNumericValue(issue.to)}`
return `${path}: ${formatNumericValue(issue?.from)} ${issue?.action ?? 'sanitized'}${to}`
})
.join('; ')
} catch {
return '<diagnostic unavailable>'
}
}

function warnSanitizedNodeMutation(
mutation: 'create' | 'update',
nodeId: AnyNodeId,
issues: NumericSanitizeIssue[],
) {
console.warn(
`[Scene] Sanitized invalid numeric node ${mutation}`,
nodeId,
numericSanitizeIssuesToMessage(issues),
)
let message = '<diagnostic unavailable>'
try {
message = numericSanitizeIssuesToMessage(issues)
} catch {
// Reporting must never interrupt a node mutation.
}

try {
console.warn(`[Scene] Sanitized invalid numeric node ${mutation}`, nodeId, message)
} catch {
// A broken diagnostic sink must not interrupt a node mutation either.
}
}

function parseCreatedNode(node: AnyNode, parentId: AnyNodeId | null): AnyNode {
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/store/actions/node-mutation-sanitize.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { beforeEach, describe, expect, test } from 'bun:test'
import type { AnyNode, AnyNodeId } from '../../schema/types'
import useScene from '../use-scene'
import { numericSanitizeIssuesToMessage } from './node-actions'

type RafFn = (cb: (t: number) => void) => number
;(globalThis as unknown as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= ((
Expand Down Expand Up @@ -87,6 +88,21 @@ function shelf() {
return useScene.getState().nodes[SHELF_ID] as Extract<AnyNode, { type: 'shelf' }>
}

describe('numeric sanitization diagnostics', () => {
test('formats missing and non-array issue paths defensively', () => {
const issues = [
{ from: Infinity, action: 'dropped' },
{ path: 'width', from: Number.NaN, action: 'dropped' },
] as never

expect(numericSanitizeIssuesToMessage(issues)).toBe(
'<unknown>: Infinity dropped; <unknown>: NaN dropped',
)
expect(numericSanitizeIssuesToMessage(null)).toBe('')
expect(numericSanitizeIssuesToMessage(undefined)).toBe('')
})
})

describe('node mutation numeric sanitization', () => {
beforeEach(() => {
useScene.setState({
Expand Down Expand Up @@ -171,6 +187,29 @@ describe('node mutation numeric sanitization', () => {
expect(panel.name).toBe('Updated panel')
})

test('updateNodes continues through schema-invalid numeric updates when reporting throws', () => {
const originalConsoleWarn = console.warn
console.warn = () => {
throw new Error('diagnostic sink failed')
}

try {
useScene.getState().updateNodes([
{ id: SHELF_ID, data: { width: Infinity } as Partial<AnyNode> },
{
id: SOLAR_PANEL_ID,
data: { name: 'Updated after invalid numeric value' } as Partial<AnyNode>,
},
])
} finally {
console.warn = originalConsoleWarn
}

expect(shelf().width).toBe(1.2)
const panel = useScene.getState().nodes[SOLAR_PANEL_ID] as { name?: string }
expect(panel.name).toBe('Updated after invalid numeric value')
})

test('sanitizes non-finite numeric values during create', () => {
const createdId = 'shelf_created' as AnyNodeId

Expand Down
Loading