Skip to content

rururux/turbo-stream-kt

Repository files navigation

Turbo Stream Kotlin

A Kotlin port of the jacob-ebey/turbo-stream decoder. This port is based on version 2.4.1 of the original jacob-ebey/turbo-stream JavaScript library. Turbo Stream is a library for serializing and deserializing JavaScript/TypeScript objects in a streaming format.

Overview

This library allows you to work with JavaScript/TypeScript objects in Kotlin using the Turbo Stream protocol, specifically targeting compatibility with streams generated by jacob-ebey/turbo-stream version 2.4.1. It provides functionality to decode JavaScript/TypeScript data sent in this streaming format and convert it to corresponding Kotlin objects. This is particularly useful for applications that need to communicate with JavaScript backends or process JavaScript data in a Kotlin multiplatform environment.

Note: This version is not compatible with streams from turbo-stream 3.0.0 or newer.

Key Features

  • Conversion of JavaScript/TypeScript primitive types and objects to their Kotlin equivalents
  • Promise support (implemented as Kotlin's Deferred)
  • Custom type decoding plugin system
  • Error handling with specific JavaScript error types
  • Support for JavaScript types like Symbol, RegExp, BigInt, URL, etc.

Type Mapping

Below is the mapping between JavaScript/TypeScript types and their Kotlin equivalents as implemented in this library:

JavaScript/TypeScript Type Kotlin Type Notes
undefined JsUndefined (object) Singleton object representing undefined.
null null
boolean Boolean
number (integer) Int
number (float) Double
string String
Date JsDate Wrapper class holding the timestamp as a Double.
NaN Double.NaN
Infinity Double.POSITIVE_INFINITY
-Infinity Double.NEGATIVE_INFINITY
-0 -0.0
BigInt JsBigInt Wrapper class holding the BigInt value as a String.
RegExp JsRegExp Wrapper class holding the pattern and flags.
Symbol JsSymbol Wrapper class holding the symbol's key if registered globally, otherwise description.
URL JsUrl Wrapper class holding the URL as a String.
Array List<Any?>
Object Map<String, Any?>
Map Map<Any?, Any?>
Set Set<Any?>
Promise (resolved) Deferred (completed) Kotlin coroutines Deferred representing the resolved value.
Promise (rejected) Deferred (completed exceptionally) Kotlin coroutines Deferred holding the rejection reason.
Error TurboStreamJsError Custom error class capturing JS error type and message.

Wrapper Classes

The library provides several wrapper classes to represent JavaScript types that don't have direct equivalents in Kotlin or require specific handling:

JsUndefined

A singleton object representing JavaScript's undefined value.

object JsUndefined

JsDate

Represents a JavaScript Date object. It stores the date as a Double timestamp (milliseconds since epoch).

data class JsDate(val timestamp: Double)

Example: JsDate(1678886400000.0)

JsUrl

Represents a JavaScript URL object. It stores the URL as a String.

data class JsUrl(val url: String)

Example: JsUrl("https://example.com")

JsBigInt

Represents a JavaScript BigInt object. It stores the BigInt value as a String to maintain precision.

data class JsBigInt(val value: String)

Example: JsBigInt("90071992547409910000")

JsRegExp

Represents a JavaScript RegExp (Regular Expression) object. It stores the pattern as a String and flags as a List<Char>.

data class JsRegExp(val pattern: String, val flags: List<Char>)

Example: JsRegExp("hello", listOf('g', 'i')) for /hello/gi

JsSymbol

Represents a JavaScript Symbol object. It stores the symbolFor string if the symbol is registered in the global symbol registry (using Symbol.for()), or a description if it's a local symbol.

data class JsSymbol(val symbolFor: String) // Or a description for local symbols

Example: JsSymbol("myGlobalSymbol")

TurboStreamJsError

Represents an error that originated from JavaScript. It includes the error message and optionally the jsErrorType (e.g., "TypeError", "ReferenceError").

class TurboStreamJsError(
    override val message: String?,
    val jsErrorType: String?
) : Error()

These wrapper classes ensure that the specific semantics of JavaScript types are preserved when decoded into the Kotlin environment.

Usage

To use the Turbo Stream Kotlin decoder, you'll primarily interact with the decode function. This function takes a Flow<String> representing the incoming stream of Turbo Stream data and an optional list of DecodePlugin instances. It returns a DecodeResult<T>, which contains the decoded value and a Deferred<Unit> that completes when the entire stream has been processed.

Here's a basic example of how to use the decoder:

import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking

// Assuming 'decode' and related classes are in scope

fun main() = runBlocking {
// Simulate a simple Turbo Stream flow
val stringFlow = flowOf(
"""["^0",1,2,{"hello":"world"}]"""
)

    try {
        val result: DecodeResult<List<Any?>> = decode(stringFlow, plugins = null)

        // The decoded value is available once the initial part of the stream is processed
        val decodedData = result.value
        println("Decoded data: $decodedData") // Output: Decoded data: [1, 2, {hello=world}]

        // Wait for the entire stream to be processed (e.g., for deferred promises)
        result.done.await()
        println("Stream processing finished.")

    } catch (e: TurboStreamSyntaxError) {
        println("Syntax error in Turbo Stream: ${e.message}")
    } catch (e: TurboStreamJsError) {
        println("JavaScript error in stream: ${e.jsErrorType} - ${e.message}")
    } catch (e: Throwable) {
        println("An unexpected error occurred: ${e.message}")
    }
}

Plugins

The library supports a plugin system to extend its decoding capabilities. A DecodePlugin is a function that takes a type identifier string and a list of arguments (decoded from the stream) and can return a Map<String, Any?> representing the custom decoded object, or null if the plugin doesn't handle that type.

The signature of a DecodePlugin is:

typealias DecodePlugin = (String, List<Any?>) -> Map<String, Any?>?

Plugins are useful for:

  • Decoding custom JavaScript classes or objects that are not standard JSON types.
  • Integrating with domain-specific data formats that might be embedded within the Turbo Stream.

You can pass a list of these plugins to the decode function. The decoder will try to use these plugins when it encounters a type it doesn't recognize natively.

Error Handling

The decoder can throw specific exceptions:

  • TurboStreamSyntaxError: Thrown when the input stream does not conform to the expected Turbo Stream syntax (e.g., invalid JSON, malformed lines).
  • TurboStreamJsError: Thrown when the stream indicates that a JavaScript error occurred during the serialization or within a Promise that was rejected. This error includes the jsErrorType (e.g., "TypeError") and the error message from JavaScript.

It's recommended to wrap calls to decode and result.done.await() in a try-catch block to handle these potential errors gracefully.

FAQ

Q: Why? (What's the use case?)

A: React Router / Remix internally uses turbo-stream for data serialization. This Kotlin decoder library enables Kotlin-based applications, including Android apps, to seamlessly consume data from backends built with React Router or Remix without requiring any special server-side modifications for data formatting. To fetch turbo-stream encoded data from such a setup, you can typically append .data to the end of the URL path (e.g., /your/apps/route.data).

Q: I'm having trouble decoding a stream. What could be the issue?

A: This Kotlin port is based on version 2.4.1 of the original jacob-ebey/turbo-stream JavaScript library. It is not compatible with streams generated by turbo-stream version 3.0.0 or newer, as the protocol and features may have changed. Please ensure the stream you are trying to decode was encoded with a compatible version.

Q: Why is this library specifically based on turbo-stream version 2.4.1?

A: Version 2.4.1 is the specific version of turbo-stream that is (or was at the time of this library's initial development) used internally by React Router / Remix.

Q: Why are RegExp flags represented as a List<Char> (e.g., listOf('g', 'i')) instead of using something like Kotlin's RegexOption?

A: The representation of RegExp flags as List<Char> (e.g., JsRegExp("pattern", listOf('g', 'm'))) was chosen to ensure broader compatibility in Kotlin Multiplatform (KMP) environments. Not all RegexOption enum values available in Kotlin/JVM are universally supported across all KMP targets (like JavaScript or Native). Storing flags as a list of characters (which directly correspond to the flags in JavaScript's RegExp constructor like 'g', 'i', 'm', 's', 'u', 'y') provides a more platform-agnostic way to handle regular expressions that originate from JavaScript. This allows the library to accurately represent the intended behavior of the JS RegExp across different Kotlin platforms.

Q: I want to decode the stream directly into my custom class or data class instances. Is this possible?

A: TODO()

Licence

MIT

About

A Kotlin port of the jacob-ebey/turbo-stream https://github.com/jacob-ebey/turbo-stream v2.4.1 decoder.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published