Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: more lenient initial sync #839

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions src/mapeo-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ import {
kRescindFullStopRequest,
} from './sync/sync-api.js'
/** @import { ProjectSettingsValue as ProjectValue } from '@comapeo/schema' */
/** @import { SetNonNullable } from 'type-fest' */
/** @import { ReadonlyDeep, SetNonNullable } from 'type-fest' */
/** @import { CoreStorage, Namespace } from './types.js' */
/** @import { DeviceInfoParam } from './schema/client.js' */
/** @import { OpenedNoiseStream } from './lib/noise-secret-stream-helpers.js' */
/** @import { State as SyncStateState } from './sync/sync-state.js' */

/** @typedef {SetNonNullable<ProjectKeys, 'encryptionKeys'>} ValidatedProjectKeys */

Expand Down Expand Up @@ -654,33 +655,22 @@ export class MapeoManager extends TypedEmitter {
project.$getOwnRole(),
project.$getProjectSettings(),
])
const {
auth: { localState: authState },
config: { localState: configState },
} = project.$sync[kSyncState].getState()
const syncState = project.$sync[kSyncState].getState()
const isRoleSynced = ownRole !== Roles.NO_ROLE
const isProjectSettingsSynced =
projectSettings !== MapeoProject.EMPTY_PROJECT_SETTINGS
// Assumes every project that someone is invited to has at least one record
// in the auth store - the row record for the invited device
const isAuthSynced = authState.want === 0 && authState.have > 0
// Assumes every project that someone is invited to has at least one record
// in the config store - defining the name of the project.
// TODO: Enforce adding a project name in the invite method
const isConfigSynced = configState.want === 0 && configState.have > 0
if (
isRoleSynced &&
isProjectSettingsSynced &&
isAuthSynced &&
isConfigSynced
isSyncStateDoneWithInitialSync(syncState)
) {
return true
}
return new Promise((resolve, reject) => {
/** @param {import('./sync/sync-state.js').State} syncState */
const onSyncState = (syncState) => {
clearTimeout(timeoutId)
if (syncState.auth.dataToSync || syncState.config.dataToSync) {
if (!isSyncStateDoneWithInitialSync(syncState)) {
timeoutId = setTimeout(onTimeout, timeoutMs)
return
}
Expand Down Expand Up @@ -870,6 +860,31 @@ export class MapeoManager extends TypedEmitter {
}
}

/**
* Is the sync state done with initial sync?
*
* Assumes every project that someone is invited to has at least one record in
* the auth store (a record for the invited device) and the config store (the
* project name).
*
* @param {ReadonlyDeep<SyncStateState>} syncState
* @returns {boolean}
*/
function isSyncStateDoneWithInitialSync(syncState) {
return (
isNamespaceDoneWithInitialSync(syncState.auth.localState) &&
isNamespaceDoneWithInitialSync(syncState.config.localState)
)
}

/**
* @param {ReadonlyDeep<{ have: number, want: number }>} namespaceSyncState
* @returns {boolean}
*/
function isNamespaceDoneWithInitialSync({ want, have }) {
return want === 0 && have > 0
}

// We use the `protomux` property of connected peers internally, but we don't
// expose it to the API. I have avoided using a private symbol for this for fear
// that we could accidentally keep references around of protomux instances,
Expand Down
Loading