Skip to content

Commit b3536e8

Browse files
igoriakovlevilya-g
authored andcommitted
Implement WasmJs target
1 parent 9988822 commit b3536e8

25 files changed

+883
-715
lines changed

build.gradle.kts

+6
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,9 @@ allprojects {
3434
compilerOptions { freeCompilerArgs.add("-Xpartial-linkage-loglevel=ERROR") }
3535
}
3636
}
37+
38+
// Disable NPM to NodeJS nightly compatibility check.
39+
// Drop this when NodeJs version that supports latest Wasm become stable
40+
tasks.withType<org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask>().configureEach {
41+
args.add("--ignore-engines")
42+
}

core/build.gradle.kts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import kotlinx.team.infra.mavenPublicationsPom
22
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
33
import java.net.URL
4-
import java.util.Locale
54
import javax.xml.parsers.DocumentBuilderFactory
65
import java.io.ByteArrayOutputStream
76
import java.io.PrintWriter
@@ -107,6 +106,16 @@ kotlin {
107106
// }
108107
}
109108

109+
wasmJs {
110+
nodejs {
111+
testTask {
112+
useMocha {
113+
timeout = "30s"
114+
}
115+
}
116+
}
117+
}
118+
110119
sourceSets.all {
111120
val suffixIndex = name.indexOfLast { it.isUpperCase() }
112121
val targetName = name.substring(0, suffixIndex)
@@ -216,6 +225,14 @@ kotlin {
216225
dependsOn(commonJsTest)
217226
}
218227

228+
val wasmJsMain by getting {
229+
dependsOn(commonJsMain)
230+
}
231+
232+
val wasmJsTest by getting {
233+
dependsOn(commonJsTest)
234+
}
235+
219236
val nativeMain by getting {
220237
dependsOn(commonMain.get())
221238
dependencies {
@@ -399,3 +416,11 @@ tasks.configureEach {
399416
enabled = false
400417
}
401418
}
419+
420+
// Drop this configuration when the Node.JS version in KGP will support wasm gc milestone 4
421+
// check it here:
422+
// https://github.com/JetBrains/kotlin/blob/master/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/js/nodejs/NodeJsRootExtension.kt
423+
with(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.apply(rootProject)) {
424+
nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2"
425+
nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary"
426+
}

core/common/src/serializers/DateTimeUnitSerializers.kt

+30-15
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ import kotlin.reflect.KClass
2121
*/
2222
public object TimeBasedDateTimeUnitSerializer: KSerializer<DateTimeUnit.TimeBased> {
2323

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

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

68-
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("DayBased") {
69-
element<Int>("days")
71+
// https://youtrack.jetbrains.com/issue/KT-63939
72+
override val descriptor: SerialDescriptor by lazy(LazyThreadSafetyMode.PUBLICATION) {
73+
buildClassSerialDescriptor("DayBased") {
74+
element<Int>("days")
75+
}
7076
}
7177

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

112-
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("MonthBased") {
113-
element<Int>("months")
118+
// https://youtrack.jetbrains.com/issue/KT-63939
119+
override val descriptor: SerialDescriptor by lazy(LazyThreadSafetyMode.PUBLICATION) {
120+
buildClassSerialDescriptor("MonthBased") {
121+
element<Int>("months")
122+
}
114123
}
115124

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

158-
private val impl = SealedClassSerializer("kotlinx.datetime.DateTimeUnit.DateBased",
159-
DateTimeUnit.DateBased::class,
160-
arrayOf(DateTimeUnit.DayBased::class, DateTimeUnit.MonthBased::class),
161-
arrayOf(DayBasedDateTimeUnitSerializer, MonthBasedDateTimeUnitSerializer))
167+
// https://youtrack.jetbrains.com/issue/KT-63939
168+
private val impl by lazy(LazyThreadSafetyMode.PUBLICATION) {
169+
SealedClassSerializer("kotlinx.datetime.DateTimeUnit.DateBased",
170+
DateTimeUnit.DateBased::class,
171+
arrayOf(DateTimeUnit.DayBased::class, DateTimeUnit.MonthBased::class),
172+
arrayOf(DayBasedDateTimeUnitSerializer, MonthBasedDateTimeUnitSerializer))
173+
}
162174

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

192-
private val impl = SealedClassSerializer("kotlinx.datetime.DateTimeUnit",
193-
DateTimeUnit::class,
194-
arrayOf(DateTimeUnit.DayBased::class, DateTimeUnit.MonthBased::class, DateTimeUnit.TimeBased::class),
195-
arrayOf(DayBasedDateTimeUnitSerializer, MonthBasedDateTimeUnitSerializer, TimeBasedDateTimeUnitSerializer))
204+
// https://youtrack.jetbrains.com/issue/KT-63939
205+
private val impl by lazy(LazyThreadSafetyMode.PUBLICATION) {
206+
SealedClassSerializer("kotlinx.datetime.DateTimeUnit",
207+
DateTimeUnit::class,
208+
arrayOf(DateTimeUnit.DayBased::class, DateTimeUnit.MonthBased::class, DateTimeUnit.TimeBased::class),
209+
arrayOf(DayBasedDateTimeUnitSerializer, MonthBasedDateTimeUnitSerializer, TimeBasedDateTimeUnitSerializer))
210+
}
196211

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

212-
}
227+
}

core/common/test/InstantTest.kt

+12
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import kotlinx.datetime.internal.*
1111
import kotlin.random.*
1212
import kotlin.test.*
1313
import kotlin.time.*
14+
import kotlin.time.Duration.Companion.days
1415
import kotlin.time.Duration.Companion.hours
1516
import kotlin.time.Duration.Companion.milliseconds
1617
import kotlin.time.Duration.Companion.nanoseconds
@@ -251,6 +252,10 @@ class InstantTest {
251252
val instant3 = instant2 - 2.hours
252253
val offset3 = instant3.offsetIn(zone)
253254
assertEquals(offset1, offset3)
255+
256+
// TODO: fails on JS
257+
// // without the minus, this test fails on JVM
258+
// (Instant.MAX - (2 * 365).days).offsetIn(zone)
254259
}
255260

256261
@Test
@@ -618,4 +623,11 @@ class InstantRangeTest {
618623
assertEquals(expected = Instant.MIN, actual = Instant.MIN - smallDuration)
619624
}
620625
}
626+
627+
@Test
628+
fun subtractInstants() {
629+
val max = Instant.fromEpochSeconds(31494816403199L)
630+
val min = Instant.fromEpochSeconds(-31619119219200L)
631+
assertEquals(max.epochSeconds - min.epochSeconds, (max - min).inWholeSeconds)
632+
}
621633
}

core/commonJs/src/DayOfWeek.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ public actual enum class DayOfWeek {
1717
SUNDAY;
1818
}
1919

20-
internal fun jsDayOfWeek.toDayOfWeek(): DayOfWeek = DayOfWeek(this.value().toInt())
20+
internal fun jsDayOfWeek.toDayOfWeek(): DayOfWeek = DayOfWeek(this.value())

core/commonJs/src/Instant.kt

+35-35
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
package kotlinx.datetime
77

8-
import kotlinx.datetime.internal.JSJoda.ZonedDateTime
98
import kotlinx.datetime.internal.JSJoda.Instant as jtInstant
109
import kotlinx.datetime.internal.JSJoda.OffsetDateTime as jtOffsetDateTime
1110
import kotlinx.datetime.internal.JSJoda.Duration as jtDuration
1211
import kotlinx.datetime.internal.JSJoda.Clock as jtClock
13-
import kotlinx.datetime.internal.JSJoda.ChronoUnit
12+
import kotlinx.datetime.internal.JSJoda.ChronoUnit as jtChronoUnit
13+
import kotlinx.datetime.internal.JSJoda.ZonedDateTime as jtZonedDateTime
1414
import kotlinx.datetime.internal.safeAdd
1515
import kotlinx.datetime.internal.*
1616
import kotlinx.datetime.serializers.InstantIso8601Serializer
@@ -40,24 +40,24 @@ public actual class Instant internal constructor(internal val value: jtInstant)
4040
}
4141

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

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

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

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

5757
override fun equals(other: Any?): Boolean =
58-
(this === other) || (other is Instant && this.value == other.value)
58+
(this === other) || (other is Instant && (this.value === other.value || this.value.equals(other.value)))
5959

60-
override fun hashCode(): Int = value.hashCode().toInt()
60+
override fun hashCode(): Int = value.hashCode()
6161

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

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

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

106106
public actual fun fromEpochSeconds(epochSeconds: Long, nanosecondAdjustment: Int): Instant = try {
107-
Instant(jtInstant.ofEpochSecond(epochSeconds, nanosecondAdjustment))
107+
Instant(jsTry { jtInstant.ofEpochSecond(epochSeconds.toDouble(), nanosecondAdjustment) })
108108
} catch (e: Throwable) {
109109
if (!e.isJodaDateTimeException()) throw e
110110
if (epochSeconds > 0) MAX else MIN
111111
}
112112

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

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

121121

122122
public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try {
123-
val thisZdt = this.value.atZone(timeZone.zoneId)
123+
val thisZdt = jsTry { this.value.atZone(timeZone.zoneId) }
124124
with(period) {
125125
thisZdt
126-
.run { if (totalMonths != 0) plusMonths(totalMonths) else this }
127-
.run { if (days != 0) plusDays(days) else this }
128-
.run { if (hours != 0) plusHours(hours) else this }
129-
.run { if (minutes != 0) plusMinutes(minutes) else this }
130-
.run { if (seconds != 0) plusSeconds(seconds) else this }
131-
.run { if (nanoseconds != 0) plusNanos(nanoseconds.toDouble()) else this }
126+
.run { if (totalMonths != 0) jsTry { plusMonths(totalMonths) } else this }
127+
.run { if (days != 0) jsTry { plusDays(days) } else this }
128+
.run { if (hours != 0) jsTry { plusHours(hours) } else this }
129+
.run { if (minutes != 0) jsTry { plusMinutes(minutes) } else this }
130+
.run { if (seconds != 0) jsTry { plusSeconds(seconds) } else this }
131+
.run { if (nanoseconds != 0) jsTry { plusNanos(nanoseconds.toDouble()) } else this }
132132
}.toInstant().let(::Instant)
133133
} catch (e: Throwable) {
134134
if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e)
135135
throw e
136136
}
137137

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

141141
@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit, timeZone)"))
142142
public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant =
@@ -150,9 +150,9 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo
150150
plus(value, unit).value.checkZone(timeZone)
151151
}
152152
is DateTimeUnit.DayBased ->
153-
thisZdt.plusDays(value.toDouble() * unit.days).toInstant()
153+
jsTry {thisZdt.plusDays(value.toDouble() * unit.days) }.toInstant()
154154
is DateTimeUnit.MonthBased ->
155-
thisZdt.plusMonths(value.toDouble() * unit.months).toInstant()
155+
jsTry { thisZdt.plusMonths(value.toDouble() * unit.months) }.toInstant()
156156
}.let(::Instant)
157157
} catch (e: Throwable) {
158158
if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e)
@@ -166,9 +166,9 @@ public actual fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZon
166166
is DateTimeUnit.TimeBased ->
167167
plus(value.toLong(), unit).value.checkZone(timeZone)
168168
is DateTimeUnit.DayBased ->
169-
thisZdt.plusDays(value.toDouble() * unit.days).toInstant()
169+
jsTry { thisZdt.plusDays(value.toDouble() * unit.days) }.toInstant()
170170
is DateTimeUnit.MonthBased ->
171-
thisZdt.plusMonths(value.toDouble() * unit.months).toInstant()
171+
jsTry { thisZdt.plusMonths(value.toDouble() * unit.months) }.toInstant()
172172
}.let(::Instant)
173173
} catch (e: Throwable) {
174174
if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e)
@@ -194,12 +194,12 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta
194194
}
195195

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

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

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

223223
internal actual fun Instant.toStringWithOffset(offset: UtcOffset): String =
224-
jtOffsetDateTime.ofInstant(this.value, offset.zoneOffset).toString()
224+
jtOffsetDateTime.ofInstant(this.value, offset.zoneOffset).toString()

core/commonJs/src/JSJodaExceptions.kt

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright 2019-2023 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime
7+
8+
internal fun Throwable.isJodaArithmeticException(): Boolean = hasJsExceptionName("ArithmeticException")
9+
internal fun Throwable.isJodaDateTimeException(): Boolean = hasJsExceptionName("DateTimeException")
10+
internal fun Throwable.isJodaDateTimeParseException(): Boolean = hasJsExceptionName("DateTimeParseException")
11+
12+
internal expect fun Throwable.hasJsExceptionName(name: String): Boolean
13+
14+
internal expect inline fun <reified T : Any> jsTry(crossinline body: () -> T): T

0 commit comments

Comments
 (0)