← Back to Blog

Idempotent re-wiring on every regen

·8 min read

Every time pyrpc dev regenerates types (on startup, on each procedure edit, on every watched module reload) it also re-runs the wiring. The tsconfig is reconfigured, and the bundler is reconfigured. Doing this on every regen only works because both operations are written to be idempotent: the hundredth run must leave the files byte-identical to the first.

The per-client loop

for client_dir in client_dirs:
    output_path = os.path.abspath(os.path.join(client_dir, "__pyrpc.ts"))
    n = _run_codegen(module, output_path, reload=reload)
    try:
        configure_tsconfig(client_dir)
    except Exception as e:
        console.print(f"[yellow]⚠ Could not configure tsconfig in {client_dir}: {e}[/yellow]")
    try:
        if not configure_bundler(client_dir):
            console.print(
                f"[yellow]⚠ Could not auto-configure bundler in {client_dir}, "
                "add a bundler alias '@pyrpc/types' → './__pyrpc.ts' ..."
            )
    except Exception as e:
        console.print(f"[yellow]⚠ Could not configure bundler in {client_dir}: {e}[/yellow]")

Generation and wiring happen in one loop, per client, in a fixed order: write the file, point the compiler at it, point the bundler at it. The two config steps have independent failure handling, a tsconfig problem and a bundler problem surface as separate warnings so a developer can fix them separately.

Tsconfig: read the current value first

configure_tsconfig never blindly writes. It probes the existing value of the compilerOptions.paths["@pyrpc/types"] key using a sentinel edit:

existing = _get_existing_value(content, ["compilerOptions", "paths", "@pyrpc/types"])
if existing is not None:
    clean_val = normalize(existing)   # strip comments + whitespace
    if clean_val == '["./__pyrpc.ts"]':
        return True                    # already correct → no write
    raise RuntimeError(
        "@pyrpc/types is already configured to point elsewhere"
    )

If the alias already matches the expected value, the function returns without writing, preserving the file's mtime and avoiding a churn loop. If the alias points somewhere else (the developer deliberately overrode it), it raises instead of fighting the override.

Reading jsonc without a JSON parser

The tsconfig is JSON with comments and trailing commas, jsonc, not JSON. Rather than parsing it, _get_existing_value uses jsonc_edit.modify to ask where the key would sit and reads the raw source at that span. It is a read-through-edit: probe with a sentinel value, intercept the returned span, extract the original text. The write path then uses apply_edits for a surgical insertion that preserves comments and formatting.

Idempotency is a system property

Three separate guards compose into the guarantee:

  • Tsconfig: the existing-value probe returns early on match.
  • Bundler: _already_aliased skips injection when the markers exist.
  • Both: writes happen only when the new content differs from the current file.

Because every regen passes through all three, the config files converge to a fixed point, and stay there. That is what makes running the wiring on every codegen step safe, which in turn is what makes the dev loop feel magical: you never configure, you only edit Python.