Skip to content

feat: hypercore11 migration support - #214

Open
RangerMauve wants to merge 29 commits into
mainfrom
feat/hypercore11-migration
Open

feat: hypercore11 migration support#214
RangerMauve wants to merge 29 commits into
mainfrom
feat/hypercore11-migration

Conversation

@RangerMauve

Copy link
Copy Markdown
Contributor

Based on the existing work we did in mobile

  • adds migration related code to index in backend
  • adds ability to retry init
  • new states for react side (including progress for migration)
  • pass avail space from kotlin/swift side
  • backend test for checking migration can work
  • native tests for state parsing

@socket-security

socket-security Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​comapeo/​core@​7.4.0 ⏵ 8.0.0-next.1781008497 +2100

View full report

@RangerMauve RangerMauve changed the title Feat/hypercore11 migration feat: hypercore11 migration support Jul 28, 2026
@github-actions github-actions Bot added the feature New feature (changelog) label Jul 28, 2026
@RangerMauve
RangerMauve marked this pull request as ready for review July 28, 2026 20:54
@RangerMauve
RangerMauve requested a review from gmaclennan July 28, 2026 20:55

@gmaclennan gmaclennan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This requires quite a few changes before we can safely merge this.

The retry behaviour currently is broken - it just falls through to forceFallback: true. I also don't understand the reasoning for only allowing a single retry - a user would want to keep freeing up space and retrying until they have enough space.

There is no API for the user to skip the retry and just use the old comapeo (fallback) - at least, not if we fix retry to actually retry instead of being "use fallback".

We can remove the iOS migration code and path - we haven't released anything on iOS so no need to release and maintain that.

See my note about testing - my oversight never plugging backend tests into CI, and it's really important that this code has some CI tests that are required before merge. The tests you added I don't think are running from any test script due to the filename glob.

There is no partial migration state handling, e.g. if migration throws an error before completing (e.g. because disk space runs out, despite our attempts to ensure there is enough space). This code allows a user to go into the fallback in this state (partially migrated) which would result in data corruption. Also if migration does fail due to disk space, this isn't exposed as a low-space error, just a generic backend error. I think this needs a fix in core too - we should detect the partial state in core and forbid opening the v7 core. Also low_space needs to indicate whether it is possible to ignore / delay until later, so the UX can decide whether to show the button that results in the fallback.

Comment thread backend/package.json Outdated
"@sentry/core": "^10.53.0",
"@sentry/node-core": "^10.53.0",
"@sentry/opentelemetry": "^10.53.0",
"comapeo-core-old": "npm:@comapeo/core@^7.1.0",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

let's pin this as we do other deps.

Comment thread src/ComapeoCore.types.ts
* accept fallback) to allow the migration to proceed. A positive value
* means the migration needs more space than is currently available.
*/
spaceNeeded?: number;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: This would be easier to work with on the front-end if it's a discriminate union, e.g.

type StateChangeEventPayload = {
  state: Exclude<ComapeoState, 'ERROR' | 'LOW_SPACE'>;
} | {
  state: 'ERROR';
  errorPhase: string;
  errorMessage: string;
} | {
  state: 'LOW_SPACE';
  spaceNeeded: number;
}

Comment thread src/ComapeoCoreModule.ts Outdated
* frame (subsequent retries after the first are no-ops in the backend).
*/
export function sendRetry(): void {
nativeModule.sendRetry?.();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why the nullish optional chaining? Wouldn't it be better to throw rather than do nothing?

Comment thread src/index.ts Outdated
comapeoServicesClient,
getNotificationPermissionsAsync,
requestNotificationPermissionsAsync,
sendRetry,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: This is confusing naming. Retry what? "send" to where? Maybe call it retryMigration()?

Comment thread backend/index.js Outdated
});
// Old and new MapeoManager have incompatible #private fields; cast
// to the newer type. Both share the same public API surface.
comapeoManager = /** @type {import("@comapeo/core").MapeoManager} */ (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Rather than cast the type, either redefine the type of comapeoManager to be either the old or the new type, or use this helper:

type Public<T> = { [K in keyof T]: T[K] }
let comapeoManager: Public<MapeoManager>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a really useful trick, I've been running into this with mocks in other codebases. 🙇

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not enough because other places ask for the full manager class. Maybe we can change the type exported in core?

is ControlFrame.Migrating -> setState(JsState.MIGRATING)
is ControlFrame.LowSpace -> setState(
JsState.LOW_SPACE,
mapOf("spaceNeeded" to frame.spaceNeeded.toString()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is sent as a string, but in JS it's typed as a number, so I don't think this will work as-is?

Comment thread src/ComapeoCoreModule.ts Outdated
Comment on lines +211 to +212
* Safe to call from any state — if not parked, the backend ignores the
* frame (subsequent retries after the first are no-ops in the backend).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this true? if sent before parked then I think it would break things. Maybe gate this on being parked?

// boot after a `LOW_SPACE` park.
Function("sendRetry") {
val availableDiskSpace =
java.nio.file.Files.getFileStore(java.nio.file.Paths.get(appContext.persistentFilesDirectory.path)).usableSpace

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This could throw an IOException - i guess ok since that just bubbles to RN JS?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think we should bubble it

SentryConfig.readApplicationMetaDataString(this, META_DEFAULT_ONLINE_STYLE_URL) ?: ""
// 6th positional: available disk space in bytes for migration decision.
val availableDiskSpace =
java.nio.file.Files.getFileStore(java.nio.file.Paths.get(dataDir)).usableSpace

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can throw a IOException and crash the FGS

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This won't work in Android API < 26, and we support API >= 24.

You can use File(dataDir).usableSpace (java.io.File) but it returns 0 on failure (rather than an error). There is also StorageManager.getAllocatableBytes(storageUuid) which is slightly different because it includes "bytes that could be written" including data Android might evict from other apps' cache folders, but it's API 26+ - we could use this with a fallback.

What do you think should be the failure mode on a device where we can't read free disk space for some reason?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

note for future self: we decided to set the val to 0 and attempt to allocate the num bytes we need in core (then delete the file).

Comment thread backend/index.js Outdated

/**
* One-shot retry gate: resolves on first `retry` frame from native.
* Only fires once so a misbehaving native side can't loop the boot.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't make sense to me, is this an agent copying from the comment below on line 103? Why would we only want to allow retry once? A user would want to keep retrying as they keep trying to free up enough space.

@RangerMauve

Copy link
Copy Markdown
Contributor Author

Going to totally redo how retries work and how we do migration to align it more with how we did it in pre core-react-native comapeo-mobile.

Remove the shared MigrationDetail struct that bundled phase/message/
spaceNeeded for both MIGRATION_ERROR and LOW_SPACE. Each state now
carries only its own data:

- MIGRATION_ERROR reuses ErrorInfo + getLastError() — the
  applyAndEmit(error:) path sets it, the module reads it via the
  existing getter. No new accessor needed.

- LOW_SPACE carries spaceNeeded (a non-nullable number) directly:
  on iOS it lives on the State enum case (dropped : String raw-value
  conformance in favour of a computed rawValue); on Android it is
  built inline in the frame handler.

The JS stateChange event now emits a StateChangeDetails union
(ComapeoErrorInfo | LowSpaceDetails) instead of a flat
ComapeoErrorInfo, so spaceNeeded reaches listeners as a number.
Sentry breadcrumbs spread the details object directly.
@RangerMauve
RangerMauve force-pushed the feat/hypercore11-migration branch from 5e04a75 to c64fa2c Compare August 24, 2026 21:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature (changelog)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants