@@ -58,9 +58,12 @@ import org.modelix.model.mutable.asModel
5858 * Dispose should be called on this, as otherwise a regular polling will go on.
5959 *
6060 * @property client the model client to connect to the model server
61- * @property branchRef branch or repository reference
62- * @property providedScope the CoroutineScope to use for the suspendable tasks
63- * @property initialRemoteVersion the last version on the server from which we want to start the synchronization
61+ * @param branchRefOrNull branch reference, null if strictly version-based
62+ * @property idGenerator the generator for node IDs
63+ * @param providedScope the CoroutineScope to use for the suspendable tasks
64+ * @param initialRemoteVersion the last version on the server from which we want to start the synchronization
65+ * @param repositoryId the repository ID, required if [versionHash] is provided
66+ * @param versionHash the version hash to replicate, if not using a branch reference
6467 */
6568class ReplicatedModel (
6669 val client : IModelClientV2 ,
@@ -80,7 +83,7 @@ class ReplicatedModel(
8083 initialRemoteVersion: CLVersion ? = null ,
8184 ) : this (client, branchRef, idGenerator, providedScope, initialRemoteVersion, null , null )
8285
83- val branchRef: BranchReference get() = branchRefOrNull ? : throw IllegalStateException (" ReplicatedModel is in read-only version mode" )
86+ val branchRef: BranchReference get() = branchRefOrNull ? : error (" ReplicatedModel is in read-only version mode" )
8487
8588 private val scope = providedScope ? : CoroutineScope (Dispatchers .Default )
8689 private var state = State .New
@@ -98,24 +101,33 @@ class ReplicatedModel(
98101 if (branchRefOrNull != null ) {
99102 check(versionHash == null ) { " Cannot provide both branchRef and versionHash" }
100103 remoteVersion = RemoteVersionFromBranch (client, branchRefOrNull, initialRemoteVersion)
101- } else if (versionHash != null ) {
102- val repoId = repositoryId ? : throw IllegalArgumentException (" repositoryId is required when versionHash is provided" )
103- remoteVersion = RemoteVersionFromHash (client, repoId, versionHash)
104104 } else {
105- throw IllegalArgumentException (" Either branchRef or versionHash must be provided" )
105+ require(versionHash != null ) { " Either branchRef or versionHash must be provided" }
106+ require(repositoryId != null ) { " repositoryId is required when versionHash is provided" }
107+ remoteVersion = RemoteVersionFromHash (client, repositoryId, versionHash)
106108 }
107109 }
108110
109111 private fun getLocalModel (): LocalModel = checkNotNull(localModel) { " Model is not initialized yet" }
110112
113+ /* *
114+ * Returns the model as an [IMutableModel].
115+ */
111116 fun getModel (): IMutableModel {
112117 return getLocalModel().versionedModelTree.asModel()
113118 }
114119
120+ /* *
121+ * Returns the underlying [VersionedModelTree].
122+ */
115123 fun getVersionedModelTree (): IMutableModelTree = getLocalModel().versionedModelTree
116124
125+ /* *
126+ * Starts the synchronization process.
127+ * Use [dispose] to stop it.
128+ */
117129 suspend fun start (): IMutableModelTree {
118- if (state != State .New ) throw IllegalStateException ( " already started" )
130+ check (state == State .New ) { " already started" }
119131 state = State .Starting
120132
121133 if (localModel == null ) {
@@ -137,7 +149,7 @@ class ReplicatedModel(
137149 throw ex
138150 } catch (ex: Throwable ) {
139151 LOG .error(ex) { " Failed polling" }
140- nextDelayMs = (nextDelayMs * 3 / 2 ).coerceIn(1000 , 30000 )
152+ nextDelayMs = (nextDelayMs * 3 / 2 ).coerceIn(POLLING_MIN_DELAY , POLLING_MAX_DELAY )
141153 }
142154 }
143155 }
@@ -160,18 +172,23 @@ class ReplicatedModel(
160172 return getVersionedModelTree()
161173 }
162174
175+ /* *
176+ * Resets the local model to the latest version on the server.
177+ */
163178 suspend fun resetToServerVersion () {
164179 // This delegates to remoteVersion which handles pull/load
165180 val version = remoteVersion.getInitialVersion()
166181 getLocalModel().resetToVersion(version)
167182 }
168183
184+ /* *
185+ * Returns true if this [ReplicatedModel] has been disposed.
186+ */
169187 fun isDisposed (): Boolean = state == State .Disposed
170188
171- private fun checkDisposed () {
172- if (state == State .Disposed ) throw IllegalStateException (" disposed" )
173- }
174-
189+ /* *
190+ * Stops the synchronization and releases resources.
191+ */
175192 fun dispose () {
176193 if (state == State .Disposed ) return
177194 pollingJob?.cancel(" disposed" )
@@ -208,16 +225,19 @@ class ReplicatedModel(
208225 private suspend fun pushLocalChanges () {
209226 if (isDisposed()) return
210227
211- for (attempt in 1 .. 10 ) {
228+ repeat( PUSH_MAX_ATTEMPTS ) {
212229 val version = getLocalModel().createNewLocalVersion() ? : getLocalModel().getCurrentVersion()
213230 val received = remoteVersion.push(version)
214231 if (received.getContentHash() == version.getContentHash()) return
215232 remoteVersionReceived(received, version)
216233 }
217234
218- throw IllegalStateException (" Failed to push local changes after 10 attempts" )
235+ error (" Failed to push local changes after $PUSH_MAX_ATTEMPTS attempts" )
219236 }
220237
238+ /* *
239+ * Returns the current version of the local model.
240+ */
221241 fun getCurrentVersion (): CLVersion {
222242 return getLocalModel().getCurrentVersion()
223243 }
@@ -231,13 +251,22 @@ class ReplicatedModel(
231251
232252 companion object {
233253 private val LOG = KotlinLogging .logger { }
254+ private const val POLLING_MIN_DELAY = 1000L
255+ private const val POLLING_MAX_DELAY = 30000L
256+ private const val PUSH_MAX_ATTEMPTS = 10
234257 }
235258}
236259
260+ /* *
261+ * Creates a [ReplicatedModel] for the given branch.
262+ */
237263fun IModelClientV2.getReplicatedModel (branchRef : BranchReference , idGenerator : (TreeId ) -> INodeIdGenerator <INodeReference >): ReplicatedModel {
238264 return ReplicatedModel (this , branchRef, idGenerator)
239265}
240266
267+ /* *
268+ * Creates a [ReplicatedModel] for the given branch with a provided scope.
269+ */
241270fun IModelClientV2.getReplicatedModel (branchRef : BranchReference , idGenerator : (TreeId ) -> INodeIdGenerator <INodeReference >, scope : CoroutineScope ): ReplicatedModel {
242271 return ReplicatedModel (this , branchRef, idGenerator, scope)
243272}
0 commit comments