Skip to content

values: remove shared UTF-8 comparison buffers - #541

Open
leventov wants to merge 1 commit into
convex:mainfrom
convex-in-prod:compare-utf8-stateless
Open

leventov wants to merge 1 commit into
convex:mainfrom
convex-in-prod:compare-utf8-stateless

Conversation

@leventov

@leventov leventov commented Aug 30, 2026 •

Copy link
Copy Markdown

Motivation

Convex context reuse intentionally keeps a loaded module instance alive across multiple function invocations. That changes the lifetime of module-scope variables: top-level state is not discarded when one invocation returns, and can be observed by later invocations that reuse the instance. Request-derived state must therefore remain invocation-local. Otherwise the implementation has to prove, for every path and every future change, that data retained in module state can never affect or be exposed to another invocation.

compare_utf8 currently keeps two UTF-8 scratch arrays at module scope. Each comparison writes bytes derived from its input strings into those arrays, and the bytes remain there after the call until a later comparison or module teardown. The comparator is synchronous, so this is not a data race, and the stale bytes are not currently observable in its result. It is nevertheless unnecessary cross-invocation retention of request-derived data. It lengthens the data lifetime beyond the invocation boundary and makes the context-reuse contract depend on a subtle overwrite proof rather than on the simpler invariant that invocation data does not escape into module state.

Removing the arrays makes that invariant structural. The comparison state is local to the call, so it cannot survive an invocation, and future changes such as new early-return, error, reentrancy, or asynchronous call paths do not inherit a hidden module-level buffer to audit. This is a correctness and isolation improvement for reused module instances, not a claim that the old comparator currently returns incorrect values.

Summary

Remove the module-scoped UTF-8 scratch arrays. Scan ordinary UTF-16 code units with charCodeAt, resolve full code points only at surrogate boundaries, and compute the first unequal UTF-8 byte directly from code-point bit groups. Also short-circuit equal strings.

This makes comparison state invocation-local without replacing the shared buffers with per-comparison heap allocation. Avoiding encoded byte arrays and their writes also keeps the comparator hot path small. The implementation preserves the exact existing numeric results of the comparator, including its historical handling of malformed UTF-16 and UTF-16 prefix lengths.

Test plan

  • Added reference-based tests for UTF-8 encoding boundaries and exact compareValues results.
  • Covered paired and lone surrogates.
  • Compared every adjacent code-point pair in both directions.
  • Compared 25,000 deterministic arbitrary UTF-16 string pairs.
  • Ran package lint, typecheck, and build.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Compare ordinary code units with charCodeAt and resolve full code points only at surrogate boundaries. Compute the first unequal encoded byte directly from code-point bit groups, removing the module-scoped scratch arrays and the helpers that fill them.

Short-circuit equal strings and retain the comparator’s exact historical numeric results, including malformed UTF-16 and UTF-16 prefix-length behavior. Add reference-based coverage for encoding boundaries, compareValues results, paired and lone surrogates, every adjacent code-point pair, and deterministic arbitrary UTF-16 strings.
@greptile-apps

greptile-apps Bot commented Aug 30, 2026

Copy link
Copy Markdown

Greptile Summary

The PR replaces shared UTF-8 scratch buffers with direct code-point comparison while preserving exact comparator results.

  • Routes same-type strings consistently through compareUTF8.
  • Computes the first unequal UTF-8 byte without materializing byte arrays.
  • Adds boundary, surrogate, exhaustive code-point, and deterministic arbitrary-string tests.

Confidence Score: 5/5

The PR appears safe to merge with no actionable correctness, compatibility, or security issues identified.

The new comparator preserves the prior first-differing-byte semantics across UTF-8 length boundaries and malformed UTF-16 handling, while removed helper exports have no repository consumers or public export path.

Important Files Changed

Filename Overview
npm-packages/convex/src/values/compare.ts Routes strings exclusively through the UTF-8 comparator while retaining primitive comparison for bigint and boolean values.
npm-packages/convex/src/values/compare_utf8.ts Replaces module-scoped byte buffers with direct UTF-16 scanning and bit-group comparison, with no concrete behavioral regression identified.
npm-packages/convex/src/values/compare_utf8.test.ts Adds broad reference-based coverage for exact numeric results, encoding boundaries, paired and lone surrogates, and arbitrary UTF-16 strings.

Reviews (1): Last reviewed commit: "values: remove shared UTF-8 comparison b..." | Re-trigger Greptile

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No issues found across 3 files

Re-trigger cubic

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