Skip to content

refactor(util): make layer graphs opaque and composable - #46089

Open
kitlangton wants to merge 3 commits into
v2from
layer-graph
Open

refactor(util): make layer graphs opaque and composable#46089
kitlangton wants to merge 3 commits into
v2from
layer-graph

Conversation

@kitlangton

Copy link
Copy Markdown
Contributor

Why

LayerNode's normal collection-based override APIs erased the relationship between a source and its replacement, so a supposedly closed, infallible Layer could be missing services or introduce errors. Name-based substitution and publicly exposed hoisted halves added further contracts that the types could not express.

This replaces that surface with a small algebra of declarations, ordered graphs, and checked substitutions while retaining deep overrides and shared-global/fresh-Location ownership.

What Changes

Overrides are checked when constructed, before they enter a stored or concatenated collection:

const replacements: LayerNode.Replacements = [
  Config.node.replace(Config.configured({ project: false, global: false })),
]

Instance construction now ends with one closed compilation, rather than hoisting, compiling two incomplete graphs, and manually placing Layer.fresh:

LayerNode.compile(graph, {
  replacements,
  shared: Node.tags.values.global,
})
Input or operation Contract
Node Opaque, invariant replacement authority for one declaration
group([a, b]) Ordered composition exposing both roots; transitive dependencies remain private
condition ? a : b Only outputs guaranteed by every alternative are promised
node.replace(target) Exact declaration identity; all outputs retained, no new errors or unsatisfied raw-Layer inputs
Stored or conditional replacements The same checks remain enforced; every alternative must be valid
node.mapLayer(transform) Decorates the implementation without copying or losing its dependency wiring
Repeated replacements Last entry wins before traversal; unreachable defaults and superseded cycles are ignored

Groups are not replaceable nodes. Same-named sibling declarations remain independent. Explicit method receivers prevent rebinding a weaker replacement method to a stronger declaration. Erased Layer contracts cannot hide construction requirements.

Lifetime Ownership

The compiler resolves one effective graph, checks reachable cycles and incompatible wiring, then builds ordinary Effect Layers. Shared-tag subgraphs, including their dependencies, use the enclosing memo map; remaining wiring receives one fresh map per build. Compiled wiring is memoized so shared DAGs do not expand into trees during acquisition.

flowchart TD
  Build[Compiled graph build] --> Shared[Shared-tag subgraphs]
  Build --> Local[Remaining wiring]
  Shared --> Parent[Enclosing memo map]
  Local --> Fresh[Fresh memo map for this build]
  Parent --> Effect[Effect scopes own finalization]
  Fresh --> Effect
Loading

Effect continues to own resource acquisition, reference counting, and finalization. Lazy LayerMaps retain the enclosing memo-map ancestry, and internal build metadata does not replace declared service outputs. Selected shared roots remain visible in the result.

The application builder prepends a lazy Location-map default and appends caller overrides, eliminating its original-graph pre-scan. A dependency introduced by an override now receives the same default binding naturally.

Scope

This intentionally replaces the internal assembly API: raw replacement tuples, hoist, hasUnbound, and representation-field access are removed. Core, CLI, Server, SDK, and simulation callers are migrated, with a migration guide in packages/util/docs/layer-node.md; HTTP contracts and UI behavior are unchanged.

The stricter checks also make previously ambient fixture/profile dependencies explicit. PersistentPty.configured now returns a wired node, and simulation driver installation failure is explicitly fatal at the infallible platform HTTP seam.

Verification

cd packages/core
bun typecheck
bun run test

cd ../util
bun typecheck
bun run typecheck:dist

cd ../server
bun typecheck
bun run ../core/script/test.ts test
bun run probe:workerd

cd ../sdk
bun typecheck
bun run ../core/script/test.ts test

cd ../simulation
bun typecheck
bun run ../core/script/test.ts test

cd ../cli
bun typecheck
  • Core: 3,981 pass, 40 skip, 0 fail across 225 files.
  • Server: 45 pass, 3 skip, 0 fail; SDK: 26 pass; simulation: 42 pass. An initial parallel run hit an occupied OAuth callback port; isolated baseline and final serial Server reruns pass.
  • Source contract tests also run against the built Util declarations, including negative cases for variance, unions, method receivers, erased Layers, and stored replacements.
  • Runtime regressions cover replacement pruning, startup order, concurrent dependencies, DAG sharing, root visibility, and real LayerMap eviction/failure/finalization.
  • Workerd bundle probe passes without statically imported Bun builtins. Formatting and git diff --check pass; targeted lint has no errors.
  • The normal pre-push hook passes all 33 repository typecheck tasks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant