Skip to content
16 changes: 14 additions & 2 deletions lib/IsolatedCircuit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,16 @@ export class IsolatedCircuit {

this.render()

while (this._hasIncompleteAsyncEffects()) {
await new Promise((resolve) => setTimeout(resolve, 100))
while (true) {
while (this._hasIncompleteAsyncEffects()) {
await new Promise((resolve) => setTimeout(resolve, 100))
this.render()
}

if (!this._hasDirtyRenderPhases()) {
break
}

this.render()
}

Expand All @@ -218,6 +226,10 @@ export class IsolatedCircuit {
return this.children.some((child) => child._hasIncompleteAsyncEffects())
}

_hasDirtyRenderPhases(): boolean {
return this.children.some((child) => child._hasDirtyPhasesInSubtree())
}

_hasIncompleteAsyncEffectsForPhase(phase: RenderPhase): boolean {
return (this._asyncEffectIdsByPhase.get(phase)?.size ?? 0) > 0
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,10 @@ export class NormalComponent<
this.initPorts()
}

updateInitializePortsFromChildren(): void {
this.initPorts()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i suspect you don't need this because there's no need to mark this phase dirty because of the render order

Copy link
Copy Markdown
Contributor Author

@MustafaMulla29 MustafaMulla29 Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are failing if I remove this, I think if we remove this, then it requires a broader changes in other files

}

doInitialReactSubtreesRender(): void {
// Add React-based footprint subtree if provided
const fpElm = this.props.footprint
Expand Down Expand Up @@ -1551,7 +1555,7 @@ export class NormalComponent<
})
}

private async _getSupplierPartNumbers(
protected async _getSupplierPartNumbers(
partsEngine: any,
source_component: any,
footprinterString: string | undefined,
Expand Down
13 changes: 13 additions & 0 deletions lib/components/base-components/Renderable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const orderedRenderPhases = [
"InflateSubcircuitCircuitJson",
"SourceNameDuplicateComponentRemoval",
"PcbFootprintStringRender",
"StandardConnectorCircuitJsonRender",
"InitializePortsFromChildren",
"CreateNetsFromProps",
"AssignFallbackProps",
Expand Down Expand Up @@ -269,6 +270,18 @@ export abstract class Renderable implements IRenderable {
)
}

_hasDirtyPhasesInSubtree(): boolean {
if (Object.values(this.renderPhaseStates).some((phase) => phase.dirty)) {
return true
}

return this.children.some((child) =>
typeof (child as Renderable)._hasDirtyPhasesInSubtree === "function"
? (child as Renderable)._hasDirtyPhasesInSubtree()
: false,
)
}

_hasIncompleteAsyncEffectsInSubtreeForPhase(phase: RenderPhase): boolean {
// Check self
for (const e of this._asyncEffects) {
Expand Down
139 changes: 135 additions & 4 deletions lib/components/normal-components/Connector.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { guessCableInsertCenter } from "@tscircuit/infer-cable-insertion-point"
import { chipProps } from "@tscircuit/props"
import type { SourceSimpleConnector } from "circuit-json"
import { chipProps, type ConnectorProps } from "@tscircuit/props"
import type { AnyCircuitElement, SourceSimpleConnector } from "circuit-json"
import { unknown_error_finding_part } from "circuit-json"
import { createComponentsFromCircuitJson } from "lib/utils/createComponentsFromCircuitJson"
import { Chip } from "./Chip"

export class Connector<
Expand All @@ -17,19 +19,148 @@ export class Connector<
doInitialSourceRender(): void {
const { db } = this.root!
const { _parsedProps: props } = this
const connectorProps = this.props as ConnectorProps

const source_component = db.source_component.insert({
ftype: "simple_connector",
name: this.name,
manufacturer_part_number: props.manufacturerPartNumber,
manufacturer_part_number: props.manufacturerPartNumber ?? props.mfn,
supplier_part_numbers: props.supplierPartNumbers,
display_name: props.displayName,
standard: (props as any).standard,
standard: connectorProps.standard,
} as SourceSimpleConnector)

this.source_component_id = source_component.source_component_id!
}

private _isUsingStandardPartsEngineCircuitJsonFlow() {
const connectorProps = this.props as ConnectorProps
if (!connectorProps.standard) return false
if (this.getInheritedProperty("partsEngineDisabled")) return false
const partsEngine = this.getInheritedProperty("partsEngine")
return Boolean(partsEngine?.fetchPartCircuitJson)
}

doInitialStandardConnectorCircuitJsonRender(): void {
const { _parsedProps: props } = this
const connectorProps = this.props as ConnectorProps
const standard = connectorProps.standard

if (!standard) return
if (this._hasStartedFootprintUrlLoad) return
if (this.getInheritedProperty("partsEngineDisabled")) return
const partsEngine = this.getInheritedProperty("partsEngine")
if (!partsEngine?.fetchPartCircuitJson) return

this._hasStartedFootprintUrlLoad = true

// source_component_id is not yet set (SourceRender runs later), but after
// the first await in this async effect synchronous phases complete and
// source_component_id will be available for db updates.
const sourceComponentForQuery = {
ftype: "simple_connector",
name: this.name,
manufacturer_part_number: props.manufacturerPartNumber ?? props.mfn,
standard,
}

this._queueAsyncEffect("load-standard-connector-circuit-json", async () => {
const { db } = this.root!
try {
// Step 1: findPart → supplier part numbers
const supplierPartNumbers = await this._getSupplierPartNumbers(
partsEngine,
sourceComponentForQuery,
`standard:${standard}`,
)

if (this.source_component_id) {
db.source_component.update(this.source_component_id, {
supplier_part_numbers: supplierPartNumbers,
})
}

// Step 2: fetchPartCircuitJson with first available supplier part number
let circuitJson: AnyCircuitElement[] | null = null
for (const supplier of Object.keys(supplierPartNumbers ?? {})) {
const nums = supplierPartNumbers[supplier]
if (Array.isArray(nums) && nums.length > 0) {
const maybeCircuitJson =
(await Promise.resolve(
partsEngine.fetchPartCircuitJson({
supplierPartNumber: nums[0],
}),
)) ?? null
if (
Array.isArray(maybeCircuitJson) &&
maybeCircuitJson.length > 0
) {
circuitJson = maybeCircuitJson
break
}
}
}

// Fallback: manufacturer part number
if (
(!circuitJson || circuitJson.length === 0) &&
sourceComponentForQuery.manufacturer_part_number
) {
const maybeCircuitJson =
(await Promise.resolve(
partsEngine.fetchPartCircuitJson({
manufacturerPartNumber:
sourceComponentForQuery.manufacturer_part_number,
}),
)) ?? null
if (Array.isArray(maybeCircuitJson) && maybeCircuitJson.length > 0) {
circuitJson = maybeCircuitJson
}
}

if (circuitJson && circuitJson.length > 0) {
const fpComponents = createComponentsFromCircuitJson(
{
componentName: this.name,
componentRotation: String(props.pcbRotation ?? 0),
footprinterString: `standard:${standard}`,
pinLabels: props.pinLabels,
pcbPinLabels: props.pcbPinLabels,
},
circuitJson,
)
this.addAll(fpComponents)
this._markDirty("InitializePortsFromChildren")
}
} catch (error: any) {
if (this.source_component_id) {
db.source_component.update(this.source_component_id, {
supplier_part_numbers: {},
})
}
const errorObj = unknown_error_finding_part.parse({
type: "unknown_error_finding_part",
message: `Failed to fetch circuit JSON for ${this.getString()} (standard="${standard}"): ${error.message}`,
source_component_id: this.source_component_id ?? undefined,
subcircuit_id: this.getSubcircuit()?.subcircuit_id,
})
db.unknown_error_finding_part.insert(errorObj)
}
})
}

doInitialPartsEngineRender(): void {
// For standard connectors, supplier part numbers are already resolved
// during StandardConnectorCircuitJsonRender via findPart + fetchPartCircuitJson
if (this._isUsingStandardPartsEngineCircuitJsonFlow()) return
super.doInitialPartsEngineRender()
}

updatePartsEngineRender(): void {
if (this._isUsingStandardPartsEngineCircuitJsonFlow()) return
super.updatePartsEngineRender()
}

doInitialPcbComponentSizeCalculation(): void {
super.doInitialPcbComponentSizeCalculation()
if (this.root?.pcbDisabled) return
Expand Down
3 changes: 1 addition & 2 deletions lib/fiber/intrinsic-jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ export interface TscircuitElements {
constraint: Props.ConstraintProps
constrainedlayout: Props.ConstrainedLayoutProps
battery: Props.BatteryProps
// TODO use ConnectorProps once it gets merged in @tscircuit/props
connector: Props.ChipProps
connector: Props.ConnectorProps
pinheader: Props.PinHeaderProps
resonator: Props.ResonatorProps
subcircuit: Props.SubcircuitGroupProps
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"@tscircuit/math-utils": "^0.0.36",
"@tscircuit/miniflex": "^0.0.4",
"@tscircuit/ngspice-spice-engine": "^0.0.8",
"@tscircuit/props": "^0.0.499",
"@tscircuit/props": "^0.0.500",
"@tscircuit/schematic-match-adapt": "^0.0.16",
"@tscircuit/schematic-trace-solver": "^v0.0.45",
"@tscircuit/solver-utils": "^0.0.3",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { test, expect } from "bun:test"
import type { PartsEngine } from "@tscircuit/props"
import { getTestFixture } from "tests/fixtures/get-test-fixture"

test("connector usb_c leaves footprint empty when supplier and manufacturer circuit json are not found", async () => {
const { circuit } = getTestFixture()
const calls: Array<{
supplierPartNumber?: string
manufacturerPartNumber?: string
}> = []

const mockPartsEngine: PartsEngine = {
findPart: async () => ({ jlcpcb: ["C165948"] }),
fetchPartCircuitJson: async ({
supplierPartNumber,
manufacturerPartNumber,
}: {
supplierPartNumber?: string
manufacturerPartNumber?: string
}) => {
calls.push({ supplierPartNumber, manufacturerPartNumber })
return undefined
},
}

circuit.add(
<board partsEngine={mockPartsEngine} width="20mm" height="20mm">
<connector
name="USB1"
standard="usb_c"
manufacturerPartNumber="USB4135-GF-A"
/>
</board>,
)

await circuit.renderUntilSettled()

expect(calls).toEqual([
{ supplierPartNumber: "C165948", manufacturerPartNumber: undefined },
{ supplierPartNumber: undefined, manufacturerPartNumber: "USB4135-GF-A" },
])

const sourceComponent = circuit.db.source_component
.list()
.find((c: any) => c.name === "USB1")
expect(sourceComponent).toBeTruthy()
expect(sourceComponent!.supplier_part_numbers).toEqual({
jlcpcb: ["C165948"],
})

expect(circuit.db.pcb_smtpad.list().length).toBe(0)
expect(circuit.db.unknown_error_finding_part.list().length).toBe(0)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { test, expect } from "bun:test"
import type { PartsEngine } from "@tscircuit/props"
import { getTestFixture } from "tests/fixtures/get-test-fixture"

test("connector with standard='usb_c' handles findPart returning 'Not found' without creating footprint", async () => {
const { circuit } = getTestFixture()
let fetchCalls = 0

const mockPartsEngine: PartsEngine = {
findPart: async () => "Not found",
fetchPartCircuitJson: async () => {
fetchCalls++
return undefined
},
} as any

circuit.add(
<board partsEngine={mockPartsEngine} width="20mm" height="20mm">
<connector name="USB1" standard="usb_c" />
</board>,
)

await circuit.renderUntilSettled()

const sourceComponent = circuit.db.source_component
.list()
.find((c: any) => c.name === "USB1")
expect(sourceComponent).toBeTruthy()
expect(sourceComponent!.supplier_part_numbers).toEqual({})

expect(fetchCalls).toBe(0)
expect(circuit.db.pcb_smtpad.list().length).toBe(0)
expect(circuit.db.unknown_error_finding_part.list().length).toBe(0)
})
Loading
Loading