Skip to content

refactor(xlsform2lstsv): per-call conversion state; synchronous internals - #77

Merged
jstet merged 1 commit into
mainfrom
refactor/62-stateless-converter
Sep 25, 2026
Merged

jstet merged 1 commit into
mainfrom
refactor/62-stateless-converter

Conversation

@jstet

@jstet jstet commented Sep 25, 2026

Copy link
Copy Markdown
Member

Closes #62.

The suspected bug was real. Two concurrent convert() calls on one instance failed. The second call's reset wiped the first call's choice lists mid-run, and it threw list 'colors' has no rows on the choices sheet.

Change:

  • Per-call state: all collaborators and per-conversion state (choices, field names, counters, buffered rows, languages) move into an internal Conversion object that convert() creates fresh for each call. XLSFormToTSVConverter keeps only its resolved options (via resolveConfig, from refactor: move XLSForm row types out of config/, simplify ConfigManager #64). The seven manual clear() resets and the collaborators' clear() methods are gone.
  • Synchronous internals: the XPath transpiler gets sync cores: xpathToLimeSurveySync, convertRelevanceSync, convertConstraintSync. TranspilerHelper, GroupEmitter, MatrixHandler and the conversion no longer await anything.
  • Public API unchanged: xpathToLimeSurvey, convertRelevance and convertConstraint stay Promise-returning. convert() still returns a Promise, and rejects on error instead of throwing synchronously.

Not done here: turning the collaborators into stateless functions, and removing the callback bags (matrixHelpers(), answerHelpers()). With per-call objects that's cosmetic; #69 (the Instrument model) is the natural place to rework them.

Tests:

  • New tests/ts/integration/converterConcurrency.test.ts: four surveys converted concurrently on one instance, compared with separate instances. It fails on main, passes here. Also covers instance reuse, and that errors reject rather than throw.
  • Every TSV snapshot is byte-identical after re-blessing. vitest: 884 passed. npm run validate is clean.

🤖 Generated with Claude Code

…nals

XLSFormToTSVConverter kept all per-conversion state on the instance and
reset it with seven clear() calls at the start of convert(). Because
convert() awaited inside every row, two concurrent calls on one instance
corrupted each other. Reproduced: the second call's reset wiped the first
call's choice lists ("list 'colors' has no rows on the choices sheet").

- The collaborators and state now belong to an internal Conversion object
  that convert() creates for each call. XLSFormToTSVConverter holds only its
  resolved options (resolveConfig), so an instance can be reused, including
  concurrently. The collaborators' clear() methods are gone.
- The internals are synchronous: the XPath transpiler gains sync cores
  (xpathToLimeSurveySync, convertRelevanceSync, convertConstraintSync). The
  public xpathToLimeSurvey/convertRelevance/convertConstraint stay
  Promise-returning, and so does convert(), which rejects on error.

Public API unchanged. Every TSV snapshot is identical.
tests/ts/integration/converterConcurrency.test.ts runs four surveys
concurrently on one instance and compares them with separate instances;
it fails on main and passes here.

Closes #62

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jstet
jstet merged commit 4b7a8de into main Sep 25, 2026
4 checks passed
@jstet
jstet deleted the refactor/62-stateless-converter branch September 25, 2026 14:49
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.

refactor(xlsform2lstsv): make XLSFormToTSVConverter stateless per conversion

1 participant