Skip to content

Commit

Permalink
Add browser hash/history navigation to wasmJs
Browse files Browse the repository at this point in the history
  • Loading branch information
cjbrooks12 committed May 17, 2024
1 parent 39a328b commit 5c2084b
Show file tree
Hide file tree
Showing 14 changed files with 487 additions and 103 deletions.
98 changes: 64 additions & 34 deletions .github/workflows/push_dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,24 @@ on:
push:
branches: ['dev']

env:
GITHUB_ACTOR: '${{ github.actor }}'
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
OSSRH_USERNAME: '${{ secrets.OSSRH_USERNAME }}'
OSSRH_PASSWORD: '${{ secrets.OSSRH_PASSWORD }}'
STAGING_PROFILE_ID: '${{ secrets.STAGING_PROFILE_ID }}'
SIGNING_KEY_ID: '${{ secrets.SIGNING_KEY_ID }}'
SIGNING_KEY: '${{ secrets.SIGNING_KEY }}'
SIGNING_PASSWORD: '${{ secrets.SIGNING_PASSWORD }}'
JB_SIGNING_KEY: '${{ secrets.JB_SIGNING_KEY }}'
JB_CHAIN: '${{ secrets.JB_CHAIN }}'
JB_PASSPHRASE: '${{ secrets.JB_PASSPHRASE }}'
JB_MARKETPLACE_TOKEN: '${{ secrets.JB_MARKETPLACE_TOKEN }}'

jobs:
testOnAll:
strategy:
fail-fast: false
matrix:
java: [17]
os: ['ubuntu-latest', 'windows-latest']
name: 'Test on ${{ matrix.os }} JDK ${{ matrix.java }}'
runs-on: '${{ matrix.os }}'
ktlintCheck:
name: 'KtlintCheck on macos-latst'
runs-on: 'macos-latest'
steps:
- uses: 'actions/checkout@v3'
with:
Expand All @@ -25,38 +34,59 @@ jobs:
uses: 'actions/setup-java@v2'
with:
distribution: 'temurin'
java-version: '${{ matrix.java }}'
java-version: '17'
- name: 'Run checks with Gradle'
run: './gradlew check --no-daemon --stacktrace'
run: './gradlew ktlintCheck --no-daemon --stacktrace'

publishArtifactsOnMacOs:
runs-on: 'macos-latest'
needs: ['testOnAll']
name: 'Build and publish snapshots'
env:
GITHUB_ACTOR: '${{ github.actor }}'
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
OSSRH_USERNAME: '${{ secrets.OSSRH_USERNAME }}'
OSSRH_PASSWORD: '${{ secrets.OSSRH_PASSWORD }}'
STAGING_PROFILE_ID: '${{ secrets.STAGING_PROFILE_ID }}'
SIGNING_KEY_ID: '${{ secrets.SIGNING_KEY_ID }}'
SIGNING_KEY: '${{ secrets.SIGNING_KEY }}'
SIGNING_PASSWORD: '${{ secrets.SIGNING_PASSWORD }}'
JB_SIGNING_KEY: '${{ secrets.JB_SIGNING_KEY }}'
JB_CHAIN: '${{ secrets.JB_CHAIN }}'
JB_PASSPHRASE: '${{ secrets.JB_PASSPHRASE }}'
JB_MARKETPLACE_TOKEN: '${{ secrets.JB_MARKETPLACE_TOKEN }}'
unitTest:
needs: ['ktlintCheck']
strategy:
matrix:
config:
- { target: 'testDebugUnitTest', os: 'ubuntu-latest', java: 17 }
- { target: 'testReleaseUnitTest', os: 'ubuntu-latest', java: 17 }
- { target: 'iosArm64Test', os: 'macos-latest', java: 17 }
- { target: 'iosSimulatorArm64Test', os: 'macos-latest', java: 17 }
- { target: 'iosX64Test', os: 'macos-latest', java: 17 }
- { target: 'jsTest', os: 'ubuntu-latest', java: 17 }
- { target: 'jvmTest', os: 'ubuntu-latest', java: 17 }
- { target: 'wasmJsTest', os: 'ubuntu-latest', java: 17 }
name: 'Run ${{ matrix.config.target }} on ${{ matrix.config.os }} JDK ${{ matrix.config.java }}'
runs-on: '${{ matrix.config.os }}'
steps:
- uses: 'actions/checkout@v3'
with:
submodules: 'recursive'
fetch-depth: 0 # all commit history and tags
- name: 'Set up JDK 17'
uses: 'actions/setup-java@v2'
- uses: 'actions/setup-java@v2'
with:
distribution: 'temurin'
java-version: ${{ matrix.config.java }}
- run: './gradlew ${{ matrix.config.target }} --stacktrace'

publishSnapshotArtifacts:
needs: ['unitTest']
strategy:
matrix:
config:
- { target: 'AndroidDebug', os: 'ubuntu-latest', java: 17 }
- { target: 'AndroidRelease', os: 'ubuntu-latest', java: 17 }
- { target: 'IosArm64', os: 'macos-latest', java: 17 }
- { target: 'IosSimulatorArm64', os: 'macos-latest', java: 17 }
- { target: 'IosX64', os: 'macos-latest', java: 17 }
- { target: 'Js', os: 'ubuntu-latest', java: 17 }
- { target: 'Jvm', os: 'ubuntu-latest', java: 17 }
- { target: 'KotlinMultiplatform', os: 'ubuntu-latest', java: 17 }
- { target: 'WasmJs', os: 'ubuntu-latest', java: 17 }
name: 'Publish ${{ matrix.config.target }} snapshot artifacts on ${{ matrix.config.os }} JDK ${{ matrix.config.java }}'
runs-on: '${{ matrix.config.os }}'
steps:
- uses: 'actions/checkout@v3'
with:
submodules: 'recursive'
fetch-depth: 0 # all commit history and tags
- uses: 'actions/setup-java@v2'
with:
distribution: 'temurin'
java-version: 17
- name: 'Assemble Artifacts'
run: './gradlew assemble -x orchidBuild --stacktrace'
- name: 'Publish Artifacts to MavenCentral Snapshots Repository'
run: './gradlew publishAllPublicationsToMavenCentralSnapshotsRepository --stacktrace'
java-version: ${{ matrix.config.java }}
- run: './gradlew publish${{ matrix.config.target }}PublicationToMavenCentralSnapshotsRepository'
38 changes: 13 additions & 25 deletions .github/workflows/push_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,30 +60,18 @@ jobs:
strategy:
matrix:
java: [17]
os: ['ubuntu-latest', 'macos-latest']
target: [
'AndroidDebug',
'AndroidRelease',
'IosArm64',
'IosSimulatorArm64',
'IosX64',
'Js',
'Jvm',
'KotlinMultiplatform',
'WasmJs',
]
exclude: # only publish ios targets from macos, everything else should be ubuntu
- { os: 'macos-latest', target: 'AndroidDebug' }
- { os: 'macos-latest', target: 'AndroidRelease' }
- { os: 'ubuntu-latest', target: 'IosArm64' }
- { os: 'ubuntu-latest', target: 'IosSimulatorArm64' }
- { os: 'ubuntu-latest', target: 'IosX64' }
- { os: 'macos-latest', target: 'Js' }
- { os: 'macos-latest', target: 'Jvm' }
- { os: 'macos-latest', target: 'KotlinMultiplatform' }
- { os: 'macos-latest', target: 'WasmJs' }
name: 'Publish ${{ matrix.target }} artifacts on ${{ matrix.os }} JDK ${{ matrix.java }}'
runs-on: '${{ matrix.os }}'
config:
- {target: 'AndroidDebug', os: 'ubuntu-latest' }
- {target: 'AndroidRelease', os: 'ubuntu-latest' }
- {target: 'IosArm64', os: 'macos-latest' }
- {target: 'IosSimulatorArm64', os: 'macos-latest' }
- {target: 'IosX64', os: 'macos-latest' }
- {target: 'Js', os: 'ubuntu-latest' }
- {target: 'Jvm', os: 'ubuntu-latest' }
- {target: 'KotlinMultiplatform', os: 'ubuntu-latest' }
- {target: 'WasmJs', os: 'ubuntu-latest' }
name: 'Publish ${{ matrix.config.target }} artifacts on ${{ matrix.config.os }} JDK ${{ matrix.java }}'
runs-on: '${{ matrix.config.os }}'
needs: ['openStagingRepo']
env:
stagingRepositoryId: ${{needs.openStagingRepo.outputs.stagingRepositoryId}}
Expand All @@ -97,7 +85,7 @@ jobs:
distribution: 'temurin'
java-version: 17
- run: 'echo "stagingRepositoryId=$(echo $stagingRepositoryId)"'
- run: './gradlew publish${{ matrix.target }}PublicationToMavenCentralRepository --stacktrace -Prelease -PorchidEnvironment=prod'
- run: './gradlew publish${{ matrix.config.target }}PublicationToMavenCentralRepository --stacktrace -Prelease -PorchidEnvironment=prod'

closeStagingRepo:
runs-on: 'ubuntu-latest'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.copperleaf.ballast.navigation.browser

import com.copperleaf.ballast.Queued
import com.copperleaf.ballast.awaitViewModelStart
import com.copperleaf.ballast.events
import com.copperleaf.ballast.navigation.internal.Uri
import com.copperleaf.ballast.navigation.internal.UriBuilder
import com.copperleaf.ballast.navigation.routing.Route
import com.copperleaf.ballast.navigation.routing.RouterContract
import com.copperleaf.ballast.navigation.routing.build
import com.copperleaf.ballast.navigation.routing.directions
import com.copperleaf.ballast.navigation.routing.mapCurrentDestination
import com.copperleaf.ballast.navigation.vm.RouterInterceptor
import com.copperleaf.ballast.navigation.vm.RouterInterceptorScope
import com.copperleaf.ballast.navigation.vm.RouterNotification
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch

@Suppress("UNUSED_PARAMETER")
public abstract class BaseBrowserNavigationInterceptor<T : Route> internal constructor(
private val initialRoute: T
) : RouterInterceptor<T> {

internal abstract fun getInitialUrl(): Uri?
internal abstract fun watchForUrlChanges(): Flow<Uri>
internal abstract fun setDestinationUrl(url: Uri)

final override fun RouterInterceptorScope<T>.start(
notifications: Flow<RouterNotification<T>>
) {
launch(start = CoroutineStart.UNDISPATCHED) {
// start by setting the initial route from the browser hash, if provided when the webpage first loads
onViewModelInitSetStateFromBrowser(notifications)

// then sync the hash state to the router state (in both directions)
joinAll(
updateBrowserOnStateChange(notifications),
updateStateOnBrowserChange(notifications)
)
}
}

private suspend fun RouterInterceptorScope<T>.onViewModelInitSetStateFromBrowser(
notifications: Flow<RouterNotification<T>>
) {
// wait for the BallastNotification.ViewModelStarted to be sent
notifications.awaitViewModelStart()

val initialDestinationUrl = getInitialUrl()?.encodedPathAndQuery
?: initialRoute.directions().build()

val deferred = CompletableDeferred<Unit>()

sendToQueue(
Queued.HandleInput(
deferred,
RouterContract.Inputs.GoToDestination(
destination = initialDestinationUrl
)
)
)

// wait for the initial URL to be set in the state, before allowing the rest of the address bar syncing to begin
deferred.await()
}

private fun RouterInterceptorScope<T>.updateBrowserOnStateChange(
notifications: Flow<RouterNotification<T>>
): Job = launch(start = CoroutineStart.UNDISPATCHED) {
notifications
.events { it }
.filterIsInstance<RouterContract.Events.BackstackChanged<T>>()
.mapNotNull { ev ->
ev.backstack.mapCurrentDestination(
route = {
if (annotations.any { it is WebEventRouteAnnotation }) {
// ignore this request
null
} else {
UriBuilder.parse(originalDestinationUrl)
}
},
notFound = { UriBuilder.parse(it) },
)
}
.distinctUntilChanged()
.onEach { destination -> setDestinationUrl(destination) }
.launchIn(this)
}

private fun RouterInterceptorScope<T>.updateStateOnBrowserChange(
notifications: Flow<RouterNotification<T>>
): Job = launch(start = CoroutineStart.UNDISPATCHED) {
watchForUrlChanges()
.onEach { destination ->
sendToQueue(
Queued.HandleInput(
null,
RouterContract.Inputs.GoToDestination(
destination = destination.encodedPathAndQuery,
extraAnnotations = setOf(WebEventRouteAnnotation),
)
)
)
}
.launchIn(this)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.copperleaf.ballast.navigation.browser

import com.copperleaf.ballast.navigation.internal.Uri
import com.copperleaf.ballast.navigation.internal.UriBuilder
import com.copperleaf.ballast.navigation.routing.Route
import kotlinx.browser.window
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import org.w3c.dom.HashChangeEvent

public class BrowserHashNavigationInterceptor<T : Route>(
initialRoute: T,
) : BaseBrowserNavigationInterceptor<T>(initialRoute) {

override fun getInitialUrl(): Uri? {
val hashValue = window.location.hash.trim().trimStart('#').trimStart('/')
val hashPieces = hashValue.split('?')

val (initialPath: String?, initialQueryString: String?) = if (hashPieces.size == 2) {
// we have path and query in the hash value
val path = hashPieces[0].takeIf { it.isNotBlank() }
val query = hashPieces[1].takeIf { it.isNotBlank() }
path to query
} else {
// only have the path in the hash value
val path = hashPieces[0].takeIf { it.isNotBlank() }
val query = window.location.search.trimStart('?').takeIf { it.isNotBlank() }
path to query
}

return if (!initialPath.isNullOrBlank() || !initialQueryString.isNullOrBlank()) {
UriBuilder.build(
encodedPath = "/$initialPath".also { println("initialPath: $it") },
encodedQueryString = initialQueryString,
)
} else {
null
}
}

override fun watchForUrlChanges(): Flow<Uri> {
return callbackFlow<Uri> {
window.onhashchange = { event: HashChangeEvent ->
val partAfterHash = event.newURL?.split("#")?.last()
if (!partAfterHash.isNullOrBlank()) {
this@callbackFlow.trySend(UriBuilder.parse(partAfterHash))
}
Unit
}

awaitClose {
window.onhashchange = null
}
}
}

override fun setDestinationUrl(url: Uri) {
window.location.hash = url.encodedPathAndQuery
}
}
Loading

0 comments on commit 5c2084b

Please sign in to comment.