Skip to content

refactor(worker-javascript): extract Phase 4 helpers from Core.js (final)#1040

Open
dqnykamp wants to merge 10 commits intoDoenet:mainfrom
dqnykamp:core-refactor-4
Open

refactor(worker-javascript): extract Phase 4 helpers from Core.js (final)#1040
dqnykamp wants to merge 10 commits intoDoenet:mainfrom
dqnykamp:core-refactor-4

Conversation

@dqnykamp
Copy link
Copy Markdown
Member

@dqnykamp dqnykamp commented May 2, 2026

Note

Stacked on #1039 (which stacks on #1038, which stacks on #1036). This PR's diff includes Phases 1, 2, and 3 until those land; review Phase 4 by reading the final commits or use the commits tab. Once the prior PRs merge, this PR will auto-rebase and the diff will show only Phase 4.

Summary

The final phase of breaking up `packages/doenetml-worker-javascript/src/Core.js`. Five remaining engines plus the `performAction`/`performUpdate` orchestrators. Same composition pattern. No behavior change.

Net effect over all four phases: `Core.js` drops from 13,837 → 1,411 lines (-89.8%). That hits the plan's end-state target almost to the line — what remains is the constructor, the `generateDast` orchestrator, the public-API delegators, and the hot-state fields.

Modules extracted in Phase 4

Module Lines Owns
`StateVariableEvaluator.ts` 1,016 `getStateVariableValue` resolution, dependency-argument gathering, change recording
`StalenessPropagator.ts` 938 Mark-stale walk through the dependency graph + on-demand `createFromArrayEntry`
`EssentialValueWriter.ts` 1,366 Inverse-definition walk + essential value writes + bulk state value re-application + composite-replacement-flush trigger
`CompositeReplacementUpdater.ts` 1,218 Recompute composite replacements when inputs change, including shadowing, withholding, and error-replacement plumbing
`UpdateExecutor.ts` 410 The `performAction` and `performUpdate` orchestrators dequeued by `ProcessQueue`

Pre-extraction discipline

After the Phase 3 binding bugs (caught only in CI), each Phase 4 cluster was pre-audited for:

  • `let core = this` captures (which break when `this` is now the manager rather than Core)
  • `function () {...}` callbacks attached to `stateVarObj` / `stateDef` (where `this` resolves to the runtime callsite object, not the manager)
  • `bind(this)` calls passing wrappers by value

All five Phase 4 clusters were clean — no patterns to work around.

One follow-up surfaced

The top-level helper `calculateAllComponentsShadowing` was used by both Core and the new `CompositeReplacementUpdater`. Moved it into `CompositeReplacementUpdater.ts` (the sole remaining caller).

Test plan

  • `npm run build -w @doenet/doenetml-worker-javascript` passes after every extraction
  • All 238 broad-regression tests pass (diagnostics + copying + baseComponent + answerValidation)
  • All 111 tests pass across the previously-failing CI files (spreadsheet / curve / curve.bezier / odesystem / functionoperators / rectangle)
  • CI: full Vitest suite + Cypress groups 1–5

Final tally across the four-PR refactor

  • 22 TypeScript modules now hold the worker logic
  • Core.js: 13,837 → 1,411 lines (-12,426, -89.8%)
  • Each module is a composed sibling holding a `core` back-reference; Core retains thin delegating wrappers for every public method/property to keep the API surface unchanged

🤖 Generated with Claude Code

dqnykamp and others added 10 commits May 1, 2026 14:14
Begin breaking up the 13,837-line Core class by lifting seven self-
contained, low-coupling helpers into TypeScript modules. The pattern
matches the existing composed siblings (Dependencies.js, ParameterStack):
each module is constructed with a `core` back-reference, and Core retains
a thin delegating wrapper for every method/property that was previously
on the class so external callers (CoreWorker, tests, components, and
`coreFunctions`-bound references) continue to work unchanged.

Modules extracted:
- DiagnosticsManager.ts       — diagnostics queue + source-location walk
- StateVariableNameResolver.ts — pure-function name resolution utilities
- VisibilityTracker.ts        — visibility state and save/suspend timers
- StatePersistence.ts         — save to localStorage / database
- AutoSubmitManager.ts        — debounced answer-submit queue
- NavigationHandler.ts        — handleNavigatingToComponent, navigateToTarget
- ResolverAdapter.ts          — adapter to the external Rust name resolver

No behavior change. Core.js drops from 13,837 to 12,909 lines.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Continues the Core.js breakup with six moderate-effort modules. Same
pattern as Phase 1 (composed sibling holding a `core` back-reference,
thin delegating wrapper on Core for every public method/property).
No behavior change.

Modules extracted:
- RendererInstructionBuilder.ts — owns componentsToRender,
  componentsWithChangedChildrenToRender, rendererState; the dast
  instruction stream sent to the renderer
- ProcessQueue.ts — owns processQueue, processing,
  stopProcessingRequests; async request queue and entry-point
  scheduling (executeProcesses, requestAction, requestUpdate,
  requestRecordEvent)
- ComponentLifecycle.ts — stateless: registration, ancestors,
  defining-child splicing, propagation to shadows
- ChildMatcher.ts — child-group matching, adapter substitution,
  rendered-child filtering (recursion guard only)
- DeletionEngine.ts — stateless two-phase component deletion
- ActionTriggerScheduler.ts — owns stateVariableChangeTriggers,
  actionsChangedToActions, originsOfActionsChangedToActions; trigger
  polling and chained-action graph

Core.js drops from 12,909 to 11,253 lines (this PR), 13,837 → 11,253
since the refactor began (~18.7%).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The deep guts of Core. Same composition pattern as Phases 1 and 2:
each module is constructed with a `core` back-reference, and Core
retains thin delegating wrappers for every public method/property.
No behavior change.

Modules extracted (in dependency order):
- StateVariableDefinitionFactory.ts — synchronous shape-building:
  attribute / adapter / reference-shadow definitions, plus the
  shadow-conversion modifiers
- StateVariableInitializer.ts — runtime initialization: lazy-resolving
  getters, dependency wiring, array-entry materialization, prop-index
  resolution
- ComponentBuilder.ts — recursive component instantiation from
  serialized DAST plus the post-creation error-component flush
- CompositeExpander.ts — composite expansion + replacement swap into
  active children + active/inactive marking; mutually recursive with
  ComponentBuilder via Core's delegators

Subtle fix: `core.publicCaseInsensitiveAliasSubstitutions.bind(this)`
calls inside CompositeExpander needed `bind(this.core)` — the wrapper
on Core uses `this.componentInfoObjects`, so the bind target must be
Core, not the manager.

Core.js drops from 11,253 to 6,063 lines (this PR), 13,837 → 6,063
since the refactor began (~56.2%).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…ndings

Two regressions from the Phase 3 extraction, surfaced in CI:

1. StateVariableInitializer.ts:425: \`let core = this;\` captured the
   manager instead of Core, so the five \`core.addDiagnostic({...})\`
   calls inside the inner array-callbacks (set up by
   \`initializeArrayStateVariable\` for setArrayValue / getArrayValue /
   etc.) failed at runtime with "core.addDiagnostic is not a function".
   Fix: \`let core = this.core;\`. Caught by functionoperators.test.ts.

2. StateVariableDefinitionFactory.ts:1299: inside
   \`stateDef.returnArrayDependenciesByKey = function () {...}\`, \`this\`
   resolves to the stateDef at call time (not the manager), and
   \`stateDef.arrayVarNameFromArrayKey\` is the method to call. The
   blanket \`this.\` → \`this.core.\` transform in the extraction script
   incorrectly added \`core.\` here. Fix: revert to
   \`this.arrayVarNameFromArrayKey(key)\`. Caught by spreadsheet,
   curve, curve.bezier, odesystem, and rectangle tests.

Audited the rest of the extracted modules for the same pattern
(\`function () {...}\` callbacks attached to stateVarObj/stateDef with
\`this.core.X\` references inside): no further occurrences.
ComponentBuilder.ts and CompositeExpander.ts have no inner-callback
patterns at all.

Verified: all 91 tests across the failing CI files plus 238 broader
regression tests pass.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
The final phase. Same composition pattern as the prior phases. With
the Phase 3 binding-bug lessons in hand, each cluster was pre-audited
for `let core = this` captures and `function () {...}` callbacks
attached to stateVarObj/stateDef before the bulk transformation; all
five Phase 4 clusters were clean. No behavior change.

Modules extracted (in dependency order):
- StateVariableEvaluator.ts — `getStateVariableValue` resolution +
  `getStateVariableDefinitionArguments` + `recordActualChangeInStateVariable`
- StalenessPropagator.ts — mark-stale walk through the dependency
  graph plus on-demand `createFromArrayEntry`
- EssentialValueWriter.ts — `processNewStateVariableValues` +
  `requestComponentChanges` + `executeUpdateStateVariables` +
  `replacementChangesFromCompositesToUpdate` + the diff-record
  calculators that feed `StatePersistence`
- CompositeReplacementUpdater.ts — `updateCompositeReplacements` and
  the supporting shadow / withhold / error-replacement plumbing
- UpdateExecutor.ts — `performAction` and `performUpdate`, the
  orchestrators dequeued by `ProcessQueue`

Side-effect: top-level helper `calculateAllComponentsShadowing` was
used by both Core and the new CompositeReplacementUpdater. Moved it
into CompositeReplacementUpdater.ts (sole remaining caller).

Core.js drops from 6,063 to 1,411 lines (this PR), 13,837 → 1,411
since the refactor began (~89.8%). The plan's end-state target was
~1,400 lines for Core, holding the constructor, the `generateDast`
orchestrator, the public-API delegators, and the hot-state fields.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 2, 2026

⚠️ No Changeset found

Latest commit: 5bead22

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant