Skip to content

perf(import): tree-shake remeda (v2 + named imports)#180

Merged
Rinse12 merged 6 commits into
masterfrom
perf/remeda-named-imports
Jul 7, 2026
Merged

perf(import): tree-shake remeda (v2 + named imports)#180
Rinse12 merged 6 commits into
masterfrom
perf/remeda-named-imports

Conversation

@Rinse12

@Rinse12 Rinse12 commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to the import-performance work (#120 / #178). Tackles the remeda step written up in #178.

Problem

Every module did import * as remeda from "remeda", and a namespace import cannot be tree-shaken, so all 164 of remeda's per-function modules landed in every static import closure — ~53% of the slim ./client entry's 309 modules (#178).

remeda v1 does not tree-shake even with named imports: its export * barrel plus the TypeScript namespace-merge .strict IIFE pattern defeat rolldown. Verified with a throwaway rolldown build — named imports of a handful of functions still pulled all 164 modules. (A re-export barrel was tried in #178 and also gave zero reduction.)

remeda v2 tree-shakes cleanly (10 modules for a 5-function import in the throwaway build), so this bumps remeda to ^2.39.0 and converts every import * as remeda site in src/ (49 files) and test/ (31 files) to specific named imports (remeda.pickpick).

Result

  • ./client static closure: 309 → 167 modules (remeda 164 → 22)
  • . index static closure: remeda 164 → 32 modules
  • Wall-clock is modest (remeda's per-module link cost is tiny). On the slow prod host (Node v22.22.2, medians of 6 warm runs): ./client import ~1274ms → ~1218ms (~4-5%); the full . index entry moved within noise (~2049ms → ~2021ms). The win is the much smaller module graph, a prerequisite for pushing ./client toward sub-second.

v2 API migration

  • .strict variants removed (strict is the default): keys.strict/fromEntries.strict/sortBy.strict/groupBy.strict drop .strict; isDefined.strict (null+undefined) → isNonNullish
  • maxBy(a, fn)firstBy(a, [fn, "desc"]), minBy(a, fn)firstBy(a, fn)
  • flatten/flattenDeepflat
  • unique<T>/difference<T> type param is the container in v2 — dropped
  • keysToOmitFromSignedPropertyNames is now an as const tuple: v2's omit/pick type key params as const Keys extends readonly ..., so a plain array leaked keys into the derived zod .pick() schema types
  • explicit annotations/casts where v2's PickFromArray/EnumeratedPartial branded return types replaced clean Pick/Omit (remote-community, rpc-local-community, signatures, util)

Runtime behavior change caught in testing (2nd commit)

v2's difference is multiset (removes each other element once) where v1's was set-based (removes all matches). The *ReservedFields lists concatenate keys from several schemas plus literals, so a field appearing twice in the candidate list and signed (present in the pubsub schema) survived as reserved — CommentModerationReservedFields regained commentModeration/communityPublicKey/communityName and VotePubsubReservedFields regained vote, which made every moderation and vote publication fail challenge verification with "has a reserved field". Fixed by wrapping the affected candidate lists in unique() (matching the guard comment-edit's list already had), plus the two runtime set-difference sites (cids-to-unpin, deleted-communities). intersection is also multiset in v2 but every call site is a .length > 0 membership check, so it is unaffected.

Local-name collisions were handled by aliasing only the 3 genuine ones (pages/util keys, db-handler entries, validatecomment clone).

Validation

  • npm run build (tsc + rolldown bundle) and config/verify-bundle.js pass; npx tsc --project test/tsconfig.json --noEmit passes
  • scripts/smoke-pack-install.js passes (pack + install + createCommunity lifecycle, incl. inlined-deps-hidden pass)
  • Suites: pkc (local-kubo-rpc + remote-pkc-rpc), rpc.errors (USE_RPC), pkc-ws-server all 22 methods (comment/vote/moderation/edit publishing), ban-then-purge moderation, vote upvote + backward-compat, comment-edit backward-compat — all green
  • Empirically diffed every *ReservedFields/*SignedPropertyNames export between the v1 and v2 builds; membership is identical after the unique() fix
  • Prod-measured on new-plebbit (deployed to a scratch install, measured, restored)

Summary by CodeRabbit

  • New Features

    • Improved import-time and startup performance by upgrading the utility library and reducing bundled module counts.
    • Reduced loaded code for core client and community workflows.
  • Bug Fixes

    • Improved signed record field handling and reserved-field checks, including updates to key-set/deduplication behavior to avoid incorrect results when fields repeat.
  • Documentation

    • Added performance and compatibility notes for the utility-library upgrade, including API/migration details and observed import-time improvements.

Rinse12 added 3 commits July 5, 2026 08:31
…king

`import * as remeda from "remeda"` cannot be tree-shaken, so all 164 of
remeda's per-function modules landed in every static import closure (~53%
of the slim ./client entry, per #178). remeda v1 does not tree-shake even
with named imports (its `export *` barrel plus the TS namespace-merge
`.strict` IIFE pattern defeat rolldown), so this bumps remeda to v2 (whose
ESM story supports it) and converts every `import * as remeda` site in src/
and test/ to specific named imports.

Result: the index static closure drops from 164 remeda modules to 32 (only
the ~30 functions actually used, plus their shared helpers).

v2 API migration:
- `.strict` variants removed (strict is the default): `keys.strict` -> `keys`,
  `fromEntries.strict` -> `fromEntries`, `sortBy.strict` -> `sortBy`,
  `groupBy.strict` -> `groupBy`
- `isDefined.strict` (narrows null+undefined) -> `isNonNullish`
- `maxBy(a, fn)` -> `firstBy(a, [fn, "desc"])`, `minBy(a, fn)` -> `firstBy(a, fn)`
- `flatten`/`flattenDeep` -> `flat`
- `unique<T>`/`difference<T>` type-param is the container in v2; dropped it
- `keysToOmitFromSignedPropertyNames` is now an `as const` readonly tuple so
  v2's `omit`/`pick` (keys typed `const Keys extends readonly ...`) remove
  keys precisely; a plain array leaked keys into the derived zod .pick() types
- explicit annotations/casts where v2's PickFromArray/EnumeratedPartial
  branded return types replaced clean Pick/Omit (remote-community,
  rpc-local-community, signatures, util)

Local-name collisions handled by aliasing only the 3 genuine ones
(pages/util `keys`, db-handler `entries`, validatecomment `clone`).
remeda v2's `difference` is multiset (removes each `other` element once),
whereas v1's was set-based (removes all matching occurrences). The
`*ReservedFields` lists concatenate keys from multiple schemas plus explicit
literals, so a field that appears twice in the candidate list AND is a signed
field (present in the pubsub schema) survived as reserved under v2 —
CommentModerationReservedFields regained commentModeration/communityPublicKey/
communityName and VotePubsubReservedFields regained `vote`. That made every
moderation and vote publication fail challenge verification with
"has a reserved field".

Wrap the affected candidate lists in `unique()` (matching the guard
comment-edit's list already had) so the multiset difference is equivalent to
v1's set difference. Also guard the two runtime set-difference sites
(ipns-publishing cids-to-unpin, util deleted-communities) the same way — the
unpin one could otherwise unpin a page cid still referenced by another sort.

`intersection` is also multiset in v2 but every call site is a `.length > 0`
membership check, so it is unaffected.
@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR upgrades remeda to v2.39.0, replaces namespace imports with named imports across source and test code, and adjusts several key, set, and selection call sites to the new helper behavior. It also updates documentation and tests to match the migration.

Changes

Remeda v2 Migration

Layer / File(s) Summary
Dependency and docs
package.json, docs/protocol/import-performance.md
Bumps remeda to v2.39.0 and records the migration notes.
Client and community modules
src/clients/base-client-manager.ts, src/community/*.ts
Switches client/community modules to named remeda helpers for key iteration, selection, omission, comparison, and gateway choice.
Pages and PKC core
src/pages/*.ts, src/pkc/*.ts
Updates page management and PKC core helpers to use named remeda utilities for key traversal, cloning, flattening, omission, and selection.
Publication schemas and RPC wiring
src/publications/**/*.ts, src/pubsub-messages/schema.ts, src/rpc/src/index.ts
Updates publication schemas, publication clients, and RPC helpers to named remeda imports and revised signed/reserved field derivation.
Runtime, DB, signer, and schema
src/runtime/**/*.ts, src/schema/schema.ts, src/signer/*.ts, src/stats.ts
Updates runtime helpers, local community logic, DB handling, schema calculations, signer helpers, and stats to named remeda functions.
Test and utility updates
src/util.ts, src/test/test-util.ts, test/**/*.ts
Updates shared utilities and test files to use named remeda imports while preserving the existing test flows and assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed It clearly summarizes the main change: upgrading remeda to v2 and switching to named imports for tree-shaking.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/remeda-named-imports

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Rinse12 added 2 commits July 5, 2026 09:02
…tance

remeda v2's clone() delegates to structuredClone() for class instances,
which throws DataCloneError on a live Comment (EventEmitter + function
refs). Spread the source into a plain object instead; createComment()
builds a fresh instance from it, so nulling raw.comment can't corrupt the
shared source. Fixes the 4 node-local failures in this suite.

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/stats.ts (1)

62-86: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

score must be synchronous here — sortBy compares the returned value directly, so it ends up sorting Promises instead of gateway scores.

Precompute the scores first (for example with Promise.all) and then sort on the resolved numeric score.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stats.ts` around lines 62 - 86, The `sortGatewaysAccordingToScore` method
is passing an async `score` callback into `sortBy`, so it sorts Promises instead
of numeric scores. Make `score` synchronous by preloading each gateway’s
failure/success data first (for example using `Promise.all` in
`sortGatewaysAccordingToScore`), then sort `gateways` using the resolved numeric
score from `_gatewayScore`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/stats.ts`:
- Around line 62-86: The `sortGatewaysAccordingToScore` method is passing an
async `score` callback into `sortBy`, so it sorts Promises instead of numeric
scores. Make `score` synchronous by preloading each gateway’s failure/success
data first (for example using `Promise.all` in `sortGatewaysAccordingToScore`),
then sort `gateways` using the resolved numeric score from `_gatewayScore`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: db3f27f8-094c-4f1a-82e1-c8f3b5cbc166

📥 Commits

Reviewing files that changed from the base of the PR and between afbbafa and be3a3c8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
📒 Files selected for processing (83)
  • docs/protocol/import-performance.md
  • package.json
  • src/clients/base-client-manager.ts
  • src/community/community-client-manager.ts
  • src/community/community-gateway-selection.ts
  • src/community/community-wire.ts
  • src/community/remote-community.ts
  • src/community/rpc-local-community.ts
  • src/community/rpc-remote-community.ts
  • src/community/schema.ts
  • src/pages/pages-client-manager.ts
  • src/pages/util.ts
  • src/pkc/pkc-client-manager.ts
  • src/pkc/pkc.ts
  • src/publications/comment-edit/schema.ts
  • src/publications/comment-moderation/schema.ts
  • src/publications/comment/comment-client-manager.ts
  • src/publications/comment/comment.ts
  • src/publications/comment/schema.ts
  • src/publications/community-edit/schema.ts
  • src/publications/publication-author.ts
  • src/publications/publication-client-manager.ts
  • src/publications/publication.ts
  • src/publications/vote/schema.ts
  • src/publications/vote/vote.ts
  • src/pubsub-messages/schema.ts
  • src/rpc/src/index.ts
  • src/runtime/browser/localforage-lru.ts
  • src/runtime/node/addresses-rewriter-proxy-server.ts
  • src/runtime/node/community/challenges/index.ts
  • src/runtime/node/community/challenges/pkc-js-challenges/publication-match.ts
  • src/runtime/node/community/db-handler.ts
  • src/runtime/node/community/local-community.ts
  • src/runtime/node/community/local-community/challenges.ts
  • src/runtime/node/community/local-community/comment-updates.ts
  • src/runtime/node/community/local-community/db-state.ts
  • src/runtime/node/community/local-community/editing.ts
  • src/runtime/node/community/local-community/ipns-publishing.ts
  • src/runtime/node/community/local-community/lifecycle.ts
  • src/runtime/node/community/local-community/publication-store.ts
  • src/runtime/node/community/local-community/publication-validation.ts
  • src/runtime/node/community/local-community/reprovide-on-address-change.ts
  • src/runtime/node/community/page-generator.ts
  • src/runtime/node/setup-kubo-address-rewriter-and-http-router.ts
  • src/runtime/node/sqlite-lru-cache.ts
  • src/runtime/node/util.ts
  • src/schema/schema.ts
  • src/signer/constants.ts
  • src/signer/signatures.ts
  • src/stats.ts
  • src/test/test-util.ts
  • src/util.ts
  • test/challenges/challenges.test.ts
  • test/challenges/exclude.test.ts
  • test/challenges/publication-match.test.ts
  • test/challenges/voucher.test.ts
  • test/node-and-browser/community/createcommunity.pkc.test.ts
  • test/node-and-browser/community/posts/pages.posts.test.ts
  • test/node-and-browser/community/update.community.test.ts
  • test/node-and-browser/community/wire-format-migration.test.ts
  • test/node-and-browser/publications/comment-edit/content.edit.test.ts
  • test/node-and-browser/publications/comment-edit/delete.edit.test.ts
  • test/node-and-browser/publications/comment-moderation/pin.test.ts
  • test/node-and-browser/publications/comment-moderation/purged.test.ts
  • test/node-and-browser/publications/comment-moderation/remove.test.ts
  • test/node-and-browser/publications/comment/backward.compatibility.commentupdate.test.ts
  • test/node-and-browser/publications/comment/publish/publish.verification.test.ts
  • test/node-and-browser/publications/community-edit/community.edit.publication.test.ts
  • test/node-and-browser/publications/vote/downvote.test.ts
  • test/node-and-browser/publications/vote/upvote.test.ts
  • test/node-and-browser/pubsub-msgs/backward.compatibility.pubsub.test.ts
  • test/node-and-browser/signatures/comment.test.ts
  • test/node-and-browser/signatures/community.test.ts
  • test/node-and-browser/signatures/edit.comment.test.ts
  • test/node-and-browser/signatures/pages.test.ts
  • test/node-and-browser/signatures/pubsub.messages.test.ts
  • test/node-and-browser/signatures/vote.test.ts
  • test/node/comment/post/pages.posts.test.ts
  • test/node/community/create.community.test.ts
  • test/node/community/edit.community.test.ts
  • test/node/community/page-generation/edgecases.page.generation.community.test.ts
  • test/node/community/purge-reserved-fields.db.community.test.ts
  • test/node/pkc/validatecomment.pkc.test.ts
💤 Files with no reviewable changes (1)
  • src/publications/vote/vote.ts

The comment.stop() abort tests asserted expect(errors).to.have.length(0),
but the update loop legitimately emits a retriable
ERR_FAILED_TO_FETCH_COMMENT_UPDATE_FROM_ALL_POST_UPDATES_RANGES error while
polling for the freshly-published comment's CommentUpdate (not yet propagated
to the remote gateway). On slow configs (firefox + remote-ipfs-gateway) that
error lands in the update()->stop() window, flaking the assertion. The abort
itself is correctly swallowed and never emits. Filter out the orthogonal
transient error before asserting the abort surfaced nothing.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
test/node-and-browser/publications/comment/stop.comment.test.ts (1)

15-26: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a runtime instanceof check instead of a type assertion.

(e as PKCError).code is a compile-time-only assertion; it doesn't verify e is actually a PKCError at runtime. It happens to be safe here since a non-PKCError would just have .code === undefined (still excluded correctly), but it's fragile if error shapes change. Since PKCError is currently import type-only, using instanceof would require importing the actual class instead.

♻️ Optional refactor using a runtime check
-import type { PKCError } from "../../../../dist/node/pkc-error.js";
+import { PKCError } from "../../../../dist/node/pkc-error.js";
...
 function errorsExcludingTransientUpdateFetchFailures(errors: Error[]) {
-    return errors.filter((e) => (e as PKCError).code !== "ERR_FAILED_TO_FETCH_COMMENT_UPDATE_FROM_ALL_POST_UPDATES_RANGES");
+    return errors.filter(
+        (e) => !(e instanceof PKCError && e.code === "ERR_FAILED_TO_FETCH_COMMENT_UPDATE_FROM_ALL_POST_UPDATES_RANGES")
+    );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/node-and-browser/publications/comment/stop.comment.test.ts` around lines
15 - 26, The transient-error filter in
errorsExcludingTransientUpdateFetchFailures currently relies on a type assertion
instead of a runtime check. Replace the compile-time-only cast on e with a real
instanceof check against PKCError, which means switching the current import to a
runtime class import in stop.comment.test.ts. Keep the filter behavior the same
by still excluding only errors whose code matches
ERR_FAILED_TO_FETCH_COMMENT_UPDATE_FROM_ALL_POST_UPDATES_RANGES.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/node-and-browser/publications/comment/stop.comment.test.ts`:
- Around line 15-26: The transient-error filter in
errorsExcludingTransientUpdateFetchFailures currently relies on a type assertion
instead of a runtime check. Replace the compile-time-only cast on e with a real
instanceof check against PKCError, which means switching the current import to a
runtime class import in stop.comment.test.ts. Keep the filter behavior the same
by still excluding only errors whose code matches
ERR_FAILED_TO_FETCH_COMMENT_UPDATE_FROM_ALL_POST_UPDATES_RANGES.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 82868014-e922-4a88-8712-d3aaba0ec4dc

📥 Commits

Reviewing files that changed from the base of the PR and between be3a3c8 and 84ee2eb.

📒 Files selected for processing (1)
  • test/node-and-browser/publications/comment/stop.comment.test.ts

@Rinse12 Rinse12 merged commit 0707680 into master Jul 7, 2026
15 checks passed
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