Skip to content

Commit

Permalink
Implement WasmJs target
Browse files Browse the repository at this point in the history
  • Loading branch information
igoriakovlev authored and ilya-g committed Dec 1, 2023
1 parent 87cc920 commit 841b0a8
Show file tree
Hide file tree
Showing 25 changed files with 883 additions and 715 deletions.
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ allprojects {
compilerOptions { freeCompilerArgs.add("-Xpartial-linkage-loglevel=ERROR") }
}
}

// Disable NPM to NodeJS nightly compatibility check.
// Drop this when NodeJs version that supports latest Wasm become stable
tasks.withType<org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask>().configureEach {
args.add("--ignore-engines")
}
27 changes: 26 additions & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import kotlinx.team.infra.mavenPublicationsPom
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.net.URL
import java.util.Locale
import javax.xml.parsers.DocumentBuilderFactory
import java.io.ByteArrayOutputStream
import java.io.PrintWriter
Expand Down Expand Up @@ -107,6 +106,16 @@ kotlin {
// }
}

wasmJs {
nodejs {
testTask {
useMocha {
timeout = "30s"
}
}
}
}

sourceSets.all {
val suffixIndex = name.indexOfLast { it.isUpperCase() }
val targetName = name.substring(0, suffixIndex)
Expand Down Expand Up @@ -216,6 +225,14 @@ kotlin {
dependsOn(commonJsTest)
}

val wasmJsMain by getting {
dependsOn(commonJsMain)
}

val wasmJsTest by getting {
dependsOn(commonJsTest)
}

val nativeMain by getting {
dependsOn(commonMain.get())
dependencies {
Expand Down Expand Up @@ -399,3 +416,11 @@ tasks.configureEach {
enabled = false
}
}

// Drop this configuration when the Node.JS version in KGP will support wasm gc milestone 4
// check it here:
// https://github.com/JetBrains/kotlin/blob/master/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt
with(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.apply(rootProject)) {
nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2"
nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary"
}
45 changes: 30 additions & 15 deletions core/common/src/serializers/DateTimeUnitSerializers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ import kotlin.reflect.KClass
*/
public object TimeBasedDateTimeUnitSerializer: KSerializer<DateTimeUnit.TimeBased> {

override val descriptor: SerialDescriptor = buildClassSerialDescriptor("TimeBased") {
element<Long>("nanoseconds")
// https://youtrack.jetbrains.com/issue/KT-63939
override val descriptor: SerialDescriptor by lazy(LazyThreadSafetyMode.PUBLICATION) {
buildClassSerialDescriptor("TimeBased") {
element<Long>("nanoseconds")
}
}

override fun serialize(encoder: Encoder, value: DateTimeUnit.TimeBased) {
Expand Down Expand Up @@ -65,8 +68,11 @@ public object TimeBasedDateTimeUnitSerializer: KSerializer<DateTimeUnit.TimeBase
*/
public object DayBasedDateTimeUnitSerializer: KSerializer<DateTimeUnit.DayBased> {

override val descriptor: SerialDescriptor = buildClassSerialDescriptor("DayBased") {
element<Int>("days")
// https://youtrack.jetbrains.com/issue/KT-63939
override val descriptor: SerialDescriptor by lazy(LazyThreadSafetyMode.PUBLICATION) {
buildClassSerialDescriptor("DayBased") {
element<Int>("days")
}
}

override fun serialize(encoder: Encoder, value: DateTimeUnit.DayBased) {
Expand Down Expand Up @@ -109,8 +115,11 @@ public object DayBasedDateTimeUnitSerializer: KSerializer<DateTimeUnit.DayBased>
*/
public object MonthBasedDateTimeUnitSerializer: KSerializer<DateTimeUnit.MonthBased> {

override val descriptor: SerialDescriptor = buildClassSerialDescriptor("MonthBased") {
element<Int>("months")
// https://youtrack.jetbrains.com/issue/KT-63939
override val descriptor: SerialDescriptor by lazy(LazyThreadSafetyMode.PUBLICATION) {
buildClassSerialDescriptor("MonthBased") {
element<Int>("months")
}
}

override fun serialize(encoder: Encoder, value: DateTimeUnit.MonthBased) {
Expand Down Expand Up @@ -155,10 +164,13 @@ public object MonthBasedDateTimeUnitSerializer: KSerializer<DateTimeUnit.MonthBa
@OptIn(InternalSerializationApi::class)
public object DateBasedDateTimeUnitSerializer: AbstractPolymorphicSerializer<DateTimeUnit.DateBased>() {

private val impl = SealedClassSerializer("kotlinx.datetime.DateTimeUnit.DateBased",
DateTimeUnit.DateBased::class,
arrayOf(DateTimeUnit.DayBased::class, DateTimeUnit.MonthBased::class),
arrayOf(DayBasedDateTimeUnitSerializer, MonthBasedDateTimeUnitSerializer))
// https://youtrack.jetbrains.com/issue/KT-63939
private val impl by lazy(LazyThreadSafetyMode.PUBLICATION) {
SealedClassSerializer("kotlinx.datetime.DateTimeUnit.DateBased",
DateTimeUnit.DateBased::class,
arrayOf(DateTimeUnit.DayBased::class, DateTimeUnit.MonthBased::class),
arrayOf(DayBasedDateTimeUnitSerializer, MonthBasedDateTimeUnitSerializer))
}

@InternalSerializationApi
override fun findPolymorphicSerializerOrNull(decoder: CompositeDecoder, klassName: String?):
Expand Down Expand Up @@ -189,10 +201,13 @@ public object DateBasedDateTimeUnitSerializer: AbstractPolymorphicSerializer<Dat
@OptIn(InternalSerializationApi::class)
public object DateTimeUnitSerializer: AbstractPolymorphicSerializer<DateTimeUnit>() {

private val impl = SealedClassSerializer("kotlinx.datetime.DateTimeUnit",
DateTimeUnit::class,
arrayOf(DateTimeUnit.DayBased::class, DateTimeUnit.MonthBased::class, DateTimeUnit.TimeBased::class),
arrayOf(DayBasedDateTimeUnitSerializer, MonthBasedDateTimeUnitSerializer, TimeBasedDateTimeUnitSerializer))
// https://youtrack.jetbrains.com/issue/KT-63939
private val impl by lazy(LazyThreadSafetyMode.PUBLICATION) {
SealedClassSerializer("kotlinx.datetime.DateTimeUnit",
DateTimeUnit::class,
arrayOf(DateTimeUnit.DayBased::class, DateTimeUnit.MonthBased::class, DateTimeUnit.TimeBased::class),
arrayOf(DayBasedDateTimeUnitSerializer, MonthBasedDateTimeUnitSerializer, TimeBasedDateTimeUnitSerializer))
}

@InternalSerializationApi
override fun findPolymorphicSerializerOrNull(decoder: CompositeDecoder, klassName: String?): DeserializationStrategy<DateTimeUnit>? =
Expand All @@ -209,4 +224,4 @@ public object DateTimeUnitSerializer: AbstractPolymorphicSerializer<DateTimeUnit
override val descriptor: SerialDescriptor
get() = impl.descriptor

}
}
12 changes: 12 additions & 0 deletions core/common/test/InstantTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import kotlinx.datetime.internal.*
import kotlin.random.*
import kotlin.test.*
import kotlin.time.*
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.nanoseconds
Expand Down Expand Up @@ -251,6 +252,10 @@ class InstantTest {
val instant3 = instant2 - 2.hours
val offset3 = instant3.offsetIn(zone)
assertEquals(offset1, offset3)

// TODO: fails on JS
// // without the minus, this test fails on JVM
// (Instant.MAX - (2 * 365).days).offsetIn(zone)
}

@Test
Expand Down Expand Up @@ -618,4 +623,11 @@ class InstantRangeTest {
assertEquals(expected = Instant.MIN, actual = Instant.MIN - smallDuration)
}
}

@Test
fun subtractInstants() {
val max = Instant.fromEpochSeconds(31494816403199L)
val min = Instant.fromEpochSeconds(-31619119219200L)
assertEquals(max.epochSeconds - min.epochSeconds, (max - min).inWholeSeconds)
}
}
2 changes: 1 addition & 1 deletion core/commonJs/src/DayOfWeek.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ public actual enum class DayOfWeek {
SUNDAY;
}

internal fun jsDayOfWeek.toDayOfWeek(): DayOfWeek = DayOfWeek(this.value().toInt())
internal fun jsDayOfWeek.toDayOfWeek(): DayOfWeek = DayOfWeek(this.value())
70 changes: 35 additions & 35 deletions core/commonJs/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

package kotlinx.datetime

import kotlinx.datetime.internal.JSJoda.ZonedDateTime
import kotlinx.datetime.internal.JSJoda.Instant as jtInstant
import kotlinx.datetime.internal.JSJoda.OffsetDateTime as jtOffsetDateTime
import kotlinx.datetime.internal.JSJoda.Duration as jtDuration
import kotlinx.datetime.internal.JSJoda.Clock as jtClock
import kotlinx.datetime.internal.JSJoda.ChronoUnit
import kotlinx.datetime.internal.JSJoda.ChronoUnit as jtChronoUnit
import kotlinx.datetime.internal.JSJoda.ZonedDateTime as jtZonedDateTime
import kotlinx.datetime.internal.safeAdd
import kotlinx.datetime.internal.*
import kotlinx.datetime.serializers.InstantIso8601Serializer
Expand Down Expand Up @@ -40,24 +40,24 @@ public actual class Instant internal constructor(internal val value: jtInstant)
}

internal fun plusFix(seconds: Double, nanos: Int): jtInstant {
val newSeconds = value.epochSecond().toDouble() + seconds
val newNanos = value.nano().toDouble() + nanos
return jtInstant.ofEpochSecond(newSeconds, newNanos)
val newSeconds = value.epochSecond() + seconds
val newNanos = value.nano() + nanos
return jsTry { jtInstant.ofEpochSecond(newSeconds, newNanos.toInt()) }
}

public actual operator fun minus(duration: Duration): Instant = plus(-duration)

public actual operator fun minus(other: Instant): Duration {
val diff = jtDuration.between(other.value, this.value)
return diff.seconds().toDouble().seconds + diff.nano().toDouble().nanoseconds
return diff.seconds().seconds + diff.nano().nanoseconds
}

public actual override operator fun compareTo(other: Instant): Int = this.value.compareTo(other.value).toInt()
public actual override operator fun compareTo(other: Instant): Int = this.value.compareTo(other.value)

override fun equals(other: Any?): Boolean =
(this === other) || (other is Instant && this.value == other.value)
(this === other) || (other is Instant && (this.value === other.value || this.value.equals(other.value)))

override fun hashCode(): Int = value.hashCode().toInt()
override fun hashCode(): Int = value.hashCode()

actual override fun toString(): String = value.toString()

Expand All @@ -74,7 +74,7 @@ public actual class Instant internal constructor(internal val value: jtInstant)
}

public actual fun parse(isoString: String): Instant = try {
Instant(jtOffsetDateTime.parse(fixOffsetRepresentation(isoString)).toInstant())
Instant(jsTry { jtOffsetDateTime.parse(fixOffsetRepresentation(isoString)) }.toInstant())
} catch (e: Throwable) {
if (e.isJodaDateTimeParseException()) throw DateTimeFormatException(e)
throw e
Expand All @@ -97,21 +97,21 @@ public actual class Instant internal constructor(internal val value: jtInstant)
Instant.fromEpochSeconds(0, Long.MAX_VALUE).nanosecondsOfSecond) */
val secs = safeAdd(epochSeconds, nanosecondAdjustment.floorDiv(NANOS_PER_ONE.toLong()))
val nos = nanosecondAdjustment.mod(NANOS_PER_ONE.toLong()).toInt()
Instant(jtInstant.ofEpochSecond(secs, nos))
Instant(jsTry { jtInstant.ofEpochSecond(secs.toDouble(), nos) })
} catch (e: Throwable) {
if (!e.isJodaDateTimeException() && e !is ArithmeticException) throw e
if (epochSeconds > 0) MAX else MIN
}

public actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Int): Instant = try {
Instant(jtInstant.ofEpochSecond(epochSeconds, nanosecondAdjustment))
Instant(jsTry { jtInstant.ofEpochSecond(epochSeconds.toDouble(), nanosecondAdjustment) })
} catch (e: Throwable) {
if (!e.isJodaDateTimeException()) throw e
if (epochSeconds > 0) MAX else MIN
}

public actual val DISTANT_PAST: Instant = Instant(jtInstant.ofEpochSecond(DISTANT_PAST_SECONDS, 999_999_999))
public actual val DISTANT_FUTURE: Instant = Instant(jtInstant.ofEpochSecond(DISTANT_FUTURE_SECONDS, 0))
public actual val DISTANT_PAST: Instant = Instant(jsTry { jtInstant.ofEpochSecond(DISTANT_PAST_SECONDS.toDouble(), 999_999_999) })
public actual val DISTANT_FUTURE: Instant = Instant(jsTry { jtInstant.ofEpochSecond(DISTANT_FUTURE_SECONDS.toDouble(), 0) })

internal actual val MIN: Instant = Instant(jtInstant.MIN)
internal actual val MAX: Instant = Instant(jtInstant.MAX)
Expand All @@ -120,23 +120,23 @@ public actual class Instant internal constructor(internal val value: jtInstant)


public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try {
val thisZdt = this.value.atZone(timeZone.zoneId)
val thisZdt = jsTry { this.value.atZone(timeZone.zoneId) }
with(period) {
thisZdt
.run { if (totalMonths != 0) plusMonths(totalMonths) else this }
.run { if (days != 0) plusDays(days) else this }
.run { if (hours != 0) plusHours(hours) else this }
.run { if (minutes != 0) plusMinutes(minutes) else this }
.run { if (seconds != 0) plusSeconds(seconds) else this }
.run { if (nanoseconds != 0) plusNanos(nanoseconds.toDouble()) else this }
.run { if (totalMonths != 0) jsTry { plusMonths(totalMonths) } else this }
.run { if (days != 0) jsTry { plusDays(days) } else this }
.run { if (hours != 0) jsTry { plusHours(hours) } else this }
.run { if (minutes != 0) jsTry { plusMinutes(minutes) } else this }
.run { if (seconds != 0) jsTry { plusSeconds(seconds) } else this }
.run { if (nanoseconds != 0) jsTry { plusNanos(nanoseconds.toDouble()) } else this }
}.toInstant().let(::Instant)
} catch (e: Throwable) {
if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e)
throw e
}

private fun Instant.atZone(zone: TimeZone): ZonedDateTime = value.atZone(zone.zoneId)
private fun jtInstant.checkZone(zone: TimeZone): jtInstant = apply { atZone(zone.zoneId) }
private fun Instant.atZone(zone: TimeZone): jtZonedDateTime = jsTry { value.atZone(zone.zoneId) }
private fun jtInstant.checkZone(zone: TimeZone): jtInstant = apply { jsTry { atZone(zone.zoneId) } }

@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit, timeZone)"))
public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant =
Expand All @@ -150,9 +150,9 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo
plus(value, unit).value.checkZone(timeZone)
}
is DateTimeUnit.DayBased ->
thisZdt.plusDays(value.toDouble() * unit.days).toInstant()
jsTry {thisZdt.plusDays(value.toDouble() * unit.days) }.toInstant()
is DateTimeUnit.MonthBased ->
thisZdt.plusMonths(value.toDouble() * unit.months).toInstant()
jsTry { thisZdt.plusMonths(value.toDouble() * unit.months) }.toInstant()
}.let(::Instant)
} catch (e: Throwable) {
if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e)
Expand All @@ -166,9 +166,9 @@ public actual fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZon
is DateTimeUnit.TimeBased ->
plus(value.toLong(), unit).value.checkZone(timeZone)
is DateTimeUnit.DayBased ->
thisZdt.plusDays(value.toDouble() * unit.days).toInstant()
jsTry { thisZdt.plusDays(value.toDouble() * unit.days) }.toInstant()
is DateTimeUnit.MonthBased ->
thisZdt.plusMonths(value.toDouble() * unit.months).toInstant()
jsTry { thisZdt.plusMonths(value.toDouble() * unit.months) }.toInstant()
}.let(::Instant)
} catch (e: Throwable) {
if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e)
Expand All @@ -194,12 +194,12 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta
}

public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod = try {
var thisZdt = this.value.atZone(timeZone.zoneId)
val otherZdt = other.value.atZone(timeZone.zoneId)
var thisZdt = jsTry { this.value.atZone(timeZone.zoneId) }
val otherZdt = jsTry { other.value.atZone(timeZone.zoneId) }

val months = thisZdt.until(otherZdt, ChronoUnit.MONTHS).toDouble(); thisZdt = thisZdt.plusMonths(months)
val days = thisZdt.until(otherZdt, ChronoUnit.DAYS).toDouble(); thisZdt = thisZdt.plusDays(days)
val nanoseconds = thisZdt.until(otherZdt, ChronoUnit.NANOS).toDouble()
val months = thisZdt.until(otherZdt, jtChronoUnit.MONTHS); thisZdt = jsTry { thisZdt.plusMonths(months) }
val days = thisZdt.until(otherZdt, jtChronoUnit.DAYS); thisZdt = jsTry { thisZdt.plusDays(days) }
val nanoseconds = thisZdt.until(otherZdt, jtChronoUnit.NANOS)

buildDateTimePeriod(months.toInt(), days.toInt(), nanoseconds.toLong())
} catch (e: Throwable) {
Expand All @@ -211,8 +211,8 @@ public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: Ti
val otherZdt = other.atZone(timeZone)
when(unit) {
is DateTimeUnit.TimeBased -> until(other, unit)
is DateTimeUnit.DayBased -> (thisZdt.until(otherZdt, ChronoUnit.DAYS).toDouble() / unit.days).toLong()
is DateTimeUnit.MonthBased -> (thisZdt.until(otherZdt, ChronoUnit.MONTHS).toDouble() / unit.months).toLong()
is DateTimeUnit.DayBased -> (thisZdt.until(otherZdt, jtChronoUnit.DAYS) / unit.days).toLong()
is DateTimeUnit.MonthBased -> (thisZdt.until(otherZdt, jtChronoUnit.MONTHS) / unit.months).toLong()
}
} catch (e: ArithmeticException) {
if (this < other) Long.MAX_VALUE else Long.MIN_VALUE
Expand All @@ -221,4 +221,4 @@ public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: Ti
}

internal actual fun Instant.toStringWithOffset(offset: UtcOffset): String =
jtOffsetDateTime.ofInstant(this.value, offset.zoneOffset).toString()
jtOffsetDateTime.ofInstant(this.value, offset.zoneOffset).toString()
14 changes: 14 additions & 0 deletions core/commonJs/src/JSJodaExceptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/

package kotlinx.datetime

internal fun Throwable.isJodaArithmeticException(): Boolean = hasJsExceptionName("ArithmeticException")
internal fun Throwable.isJodaDateTimeException(): Boolean = hasJsExceptionName("DateTimeException")
internal fun Throwable.isJodaDateTimeParseException(): Boolean = hasJsExceptionName("DateTimeParseException")

internal expect fun Throwable.hasJsExceptionName(name: String): Boolean

internal expect inline fun <reified T : Any> jsTry(crossinline body: () -> T): T
Loading

0 comments on commit 841b0a8

Please sign in to comment.