@kizunajs/kotlin 2.0.0-beta.20 → 2.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs
CHANGED
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as generateKotlinClient } from "./generator-
|
|
2
|
+
import { t as generateKotlinClient } from "./generator-DHi8XSWb.mjs";
|
|
3
3
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
@@ -1476,6 +1476,35 @@ const emitErrorBranches = (writer, method, context) => {
|
|
|
1476
1476
|
}
|
|
1477
1477
|
writer.line(`else -> throw ${failureRef}.Unexpected(statusCode = statusCode, data = data)`);
|
|
1478
1478
|
};
|
|
1479
|
+
const IDEMPOTENT_METHODS = new Set([
|
|
1480
|
+
"GET",
|
|
1481
|
+
"HEAD",
|
|
1482
|
+
"OPTIONS",
|
|
1483
|
+
"PUT",
|
|
1484
|
+
"DELETE"
|
|
1485
|
+
]);
|
|
1486
|
+
/**
|
|
1487
|
+
* Writes the reconnection an event stream reopens through and returns the argument handing it to `Kizuna.events`, or
|
|
1488
|
+
* nothing for a method a client may not resend.
|
|
1489
|
+
*/
|
|
1490
|
+
const emitReopen = (writer, method, operationRef, status) => {
|
|
1491
|
+
if (!IDEMPOTENT_METHODS.has(method.method)) return "";
|
|
1492
|
+
closure(writer, "val reconnection = reconnect?.let { policy ->", () => {
|
|
1493
|
+
closure(writer, "Kizuna.Reconnection(policy) { lastEventId ->", () => {
|
|
1494
|
+
writer.line("val again = requestBuilder.build().newBuilder()");
|
|
1495
|
+
writer.line("if (lastEventId.isNotEmpty()) again.header(\"Last-Event-ID\", lastEventId)");
|
|
1496
|
+
writer.line("requestInterceptor?.invoke(again)");
|
|
1497
|
+
writer.line("val reopened = Kizuna.send(client, again.build(), retry)");
|
|
1498
|
+
writer.line("val eventStream = reopened.header(\"Content-Type\")?.lowercase()?.startsWith(\"text/event-stream\") == true");
|
|
1499
|
+
writer.block("when", () => {
|
|
1500
|
+
writer.line("reopened.code == 204 -> { reopened.close(); null }");
|
|
1501
|
+
writer.line(`reopened.code == ${status} && eventStream -> reopened`);
|
|
1502
|
+
writer.line(`else -> { reopened.close(); throw ${operationRef}.Failure.Unexpected(statusCode = reopened.code, data = ByteArray(0)) }`);
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1505
|
+
});
|
|
1506
|
+
return ", reconnection";
|
|
1507
|
+
};
|
|
1479
1508
|
const emitStreamMethodTail = (writer, method, context) => {
|
|
1480
1509
|
const operationRef = `${context.clientName}.${method.operationName}`;
|
|
1481
1510
|
const stream = method.stream;
|
|
@@ -1488,7 +1517,7 @@ const emitStreamMethodTail = (writer, method, context) => {
|
|
|
1488
1517
|
else if (stream.events) {
|
|
1489
1518
|
const events = stream.events;
|
|
1490
1519
|
const elementType = streamElementType(method, context, "client");
|
|
1491
|
-
closure(writer, `val body = Kizuna.events<${elementType}>(httpResponse) { event ->`, () => {
|
|
1520
|
+
closure(writer, `val body = Kizuna.events<${elementType}>(httpResponse${emitReopen(writer, method, operationRef, stream.status)}) { event ->`, () => {
|
|
1492
1521
|
writer.block("when (event.event)", () => {
|
|
1493
1522
|
for (const event of events) {
|
|
1494
1523
|
const payloadType = resolveType(event.type, method.operationName, context);
|
|
@@ -1499,7 +1528,8 @@ const emitStreamMethodTail = (writer, method, context) => {
|
|
|
1499
1528
|
});
|
|
1500
1529
|
} else {
|
|
1501
1530
|
const elementType = streamElementType(method, context, "client");
|
|
1502
|
-
|
|
1531
|
+
const reconnection = emitReopen(writer, method, operationRef, stream.status);
|
|
1532
|
+
writer.line(`val body = Kizuna.events<${elementType}>(httpResponse${reconnection}) { event -> json.decodeFromString<${elementType}>(event.data) }`);
|
|
1503
1533
|
}
|
|
1504
1534
|
if (hasHeaders) {
|
|
1505
1535
|
for (const field of successResponse.responseHeaders) writer.line(`val ${escapeKeyword(field.name)} = httpResponse.header(${stringLiteral(field.wireName)})`);
|
|
@@ -1545,7 +1575,7 @@ const emitSubClientMethod = (writer, method, context) => {
|
|
|
1545
1575
|
};
|
|
1546
1576
|
const emitSubClientClass = (writer, group, clientName, context) => {
|
|
1547
1577
|
writer.blank();
|
|
1548
|
-
writer.block(`class ${group.className}(private val client: OkHttpClient, private val baseUrl: String, private val json: Json, private val requestContextHeaders: Map<String, String>, private val requestInterceptor: (suspend (Request.Builder) -> Unit)?, private val responseInterceptor: (suspend (Request, Response) -> Unit)?, private val retry: ${clientName}.RetryPolicy?)`, () => {
|
|
1578
|
+
writer.block(`class ${group.className}(private val client: OkHttpClient, private val baseUrl: String, private val json: Json, private val requestContextHeaders: Map<String, String>, private val requestInterceptor: (suspend (Request.Builder) -> Unit)?, private val responseInterceptor: (suspend (Request, Response) -> Unit)?, private val retry: ${clientName}.RetryPolicy?, private val reconnect: ${clientName}.ReconnectPolicy?)`, () => {
|
|
1549
1579
|
for (const method of group.methods) {
|
|
1550
1580
|
writer.blank();
|
|
1551
1581
|
emitSubClientMethod(writer, method, context);
|
|
@@ -1557,10 +1587,12 @@ const closure = (writer, opener, body) => {
|
|
|
1557
1587
|
writer.indent(body);
|
|
1558
1588
|
writer.line("}");
|
|
1559
1589
|
};
|
|
1560
|
-
const emitKizunaStreaming = (writer) => {
|
|
1590
|
+
const emitKizunaStreaming = (writer, clientName) => {
|
|
1561
1591
|
writer.blank();
|
|
1562
1592
|
writer.line("data class ServerSentEvent(val event: String?, val data: String, val id: String?, val retry: Int?)");
|
|
1563
1593
|
writer.blank();
|
|
1594
|
+
writer.line(`class Reconnection(val policy: ${clientName}.ReconnectPolicy, val reopen: suspend (String) -> Response?)`);
|
|
1595
|
+
writer.blank();
|
|
1564
1596
|
writer.block("private suspend fun readLine(source: okio.BufferedSource): String?", () => {
|
|
1565
1597
|
writer.line("return kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { source.readUtf8Line() }");
|
|
1566
1598
|
});
|
|
@@ -1587,44 +1619,83 @@ const emitKizunaStreaming = (writer) => {
|
|
|
1587
1619
|
});
|
|
1588
1620
|
});
|
|
1589
1621
|
writer.blank();
|
|
1590
|
-
writer.
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
writer.line("val
|
|
1595
|
-
writer.
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
writer.
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1622
|
+
writer.line("private val LINE_ENDINGS = okio.ByteString.of('\\r'.code.toByte(), '\\n'.code.toByte())");
|
|
1623
|
+
writer.blank();
|
|
1624
|
+
writer.block("private suspend fun readEventLine(source: okio.BufferedSource): String?", () => {
|
|
1625
|
+
closure(writer, "return kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {", () => {
|
|
1626
|
+
writer.line("val end = source.indexOfElement(LINE_ENDINGS)");
|
|
1627
|
+
writer.block("if (end == -1L)", () => {
|
|
1628
|
+
writer.line("if (source.exhausted()) null else source.readUtf8()");
|
|
1629
|
+
});
|
|
1630
|
+
writer.block("else", () => {
|
|
1631
|
+
writer.line("val line = source.readUtf8(end)");
|
|
1632
|
+
writer.line("val carriageReturn = source.readByte() == '\\r'.code.toByte()");
|
|
1633
|
+
writer.line("if (carriageReturn && source.request(1) && source.buffer[0] == '\\n'.code.toByte()) source.skip(1)");
|
|
1634
|
+
writer.line("line");
|
|
1635
|
+
});
|
|
1636
|
+
});
|
|
1637
|
+
});
|
|
1638
|
+
writer.blank();
|
|
1639
|
+
writer.block("fun serverSentEvents(response: Response, reconnect: Reconnection?): Flow<ServerSentEvent> = flow", () => {
|
|
1640
|
+
writer.line("var lastEventId = \"\"");
|
|
1641
|
+
writer.line("var reconnectionTime = reconnect?.policy?.reconnectionTime ?: 3.seconds");
|
|
1642
|
+
writer.line("var current: Response? = response");
|
|
1643
|
+
writer.line("var attempts = 0");
|
|
1644
|
+
writer.line("var failure: Throwable? = null");
|
|
1645
|
+
writer.block("while (true)", () => {
|
|
1646
|
+
closure(writer, "current?.use { open ->", () => {
|
|
1647
|
+
writer.line("val source = open.body?.source() ?: return@use");
|
|
1648
|
+
writer.line("var eventType = \"\"");
|
|
1649
|
+
writer.line("val dataLines = mutableListOf<String>()");
|
|
1650
|
+
writer.line("var pendingId = lastEventId");
|
|
1651
|
+
writer.line("var retry: Int? = null");
|
|
1652
|
+
writer.line("var firstLine = true");
|
|
1653
|
+
writer.block("while (true)", () => {
|
|
1654
|
+
writer.line("val received = try { readEventLine(source) } catch (error: java.io.IOException) { failure = error; null } ?: break");
|
|
1655
|
+
writer.line("val line = if (firstLine) received.removePrefix(\"\\uFEFF\") else received");
|
|
1656
|
+
writer.line("firstLine = false");
|
|
1657
|
+
writer.block("if (line.isEmpty())", () => {
|
|
1658
|
+
writer.line("lastEventId = pendingId");
|
|
1659
|
+
writer.block("if (dataLines.isNotEmpty())", () => {
|
|
1660
|
+
writer.line("emit(ServerSentEvent(eventType.ifEmpty { null }, dataLines.joinToString(\"\\n\"), lastEventId.ifEmpty { null }, retry))");
|
|
1661
|
+
writer.line("attempts = 0");
|
|
1662
|
+
writer.line("failure = null");
|
|
1663
|
+
writer.line("dataLines.clear()");
|
|
1664
|
+
writer.line("retry = null");
|
|
1665
|
+
});
|
|
1666
|
+
writer.line("eventType = \"\"");
|
|
1667
|
+
writer.line("continue");
|
|
1668
|
+
});
|
|
1669
|
+
writer.line("if (line.startsWith(\":\")) continue");
|
|
1670
|
+
writer.line("val separator = line.indexOf(':')");
|
|
1671
|
+
writer.line("val field = if (separator == -1) line else line.substring(0, separator)");
|
|
1672
|
+
writer.line("val value = (if (separator == -1) \"\" else line.substring(separator + 1)).removePrefix(\" \")");
|
|
1673
|
+
writer.block("when (field)", () => {
|
|
1674
|
+
writer.line("\"event\" -> eventType = value");
|
|
1675
|
+
writer.line("\"data\" -> dataLines.add(value)");
|
|
1676
|
+
writer.line("\"id\" -> if (!value.contains('\\u0000')) pendingId = value");
|
|
1677
|
+
writer.line("\"retry\" -> if (value.isNotEmpty() && value.all { character -> character in '0'..'9' }) value.toIntOrNull()?.let { milliseconds -> retry = milliseconds; reconnectionTime = minOf(milliseconds.milliseconds, 3_600_000.milliseconds) }");
|
|
1604
1678
|
});
|
|
1605
|
-
writer.line("eventType = \"\"");
|
|
1606
|
-
writer.line("continue");
|
|
1607
|
-
});
|
|
1608
|
-
writer.line("if (line.startsWith(\":\")) continue");
|
|
1609
|
-
writer.line("val separator = line.indexOf(':')");
|
|
1610
|
-
writer.line("val field = if (separator == -1) line else line.substring(0, separator)");
|
|
1611
|
-
writer.line("val value = (if (separator == -1) \"\" else line.substring(separator + 1)).removePrefix(\" \")");
|
|
1612
|
-
writer.block("when (field)", () => {
|
|
1613
|
-
writer.line("\"event\" -> eventType = value");
|
|
1614
|
-
writer.line("\"data\" -> dataLines.add(value)");
|
|
1615
|
-
writer.line("\"id\" -> if (!value.contains('\\u0000')) lastEventId = value");
|
|
1616
|
-
writer.line("\"retry\" -> if (value.isNotEmpty() && value.all { it.isDigit() }) retry = value.toIntOrNull()");
|
|
1617
1679
|
});
|
|
1618
1680
|
});
|
|
1681
|
+
writer.block("if (reconnect == null || attempts >= reconnect.policy.maximumAttempts)", () => {
|
|
1682
|
+
writer.line("failure?.let { throw it }");
|
|
1683
|
+
writer.line("return@flow");
|
|
1684
|
+
});
|
|
1685
|
+
writer.line("attempts += 1");
|
|
1686
|
+
writer.line("val ceiling = minOf(reconnect.policy.maximumDelay, reconnectionTime * Math.pow(2.0, (attempts - 1).toDouble()))");
|
|
1687
|
+
writer.line("val extra = (ceiling - reconnectionTime).coerceAtLeast(kotlin.time.Duration.ZERO)");
|
|
1688
|
+
writer.line("kotlinx.coroutines.delay(reconnectionTime + extra * Math.random())");
|
|
1689
|
+
writer.line("current = try { reconnect.reopen(lastEventId) ?: return@flow } catch (error: java.io.IOException) { failure = error; null }");
|
|
1619
1690
|
});
|
|
1620
1691
|
});
|
|
1621
1692
|
writer.blank();
|
|
1622
|
-
writer.line("fun <Event : Any> events(response: Response, decode: (ServerSentEvent) -> Event?): Flow<Event> = serverSentEvents(response).mapNotNull(decode)");
|
|
1693
|
+
writer.line("fun <Event : Any> events(response: Response, reconnect: Reconnection? = null, decode: (ServerSentEvent) -> Event?): Flow<Event> = serverSentEvents(response, reconnect).mapNotNull(decode)");
|
|
1623
1694
|
};
|
|
1624
1695
|
const emitKizunaObject = (writer, options) => {
|
|
1625
1696
|
writer.blank();
|
|
1626
1697
|
writer.block("private object Kizuna", () => {
|
|
1627
|
-
if (options.streaming) emitKizunaStreaming(writer);
|
|
1698
|
+
if (options.streaming) emitKizunaStreaming(writer, options.clientName);
|
|
1628
1699
|
writer.block("fun resolveUrl(baseUrl: String, path: String): HttpUrl.Builder", () => {
|
|
1629
1700
|
writer.line("val base = baseUrl.toHttpUrl()");
|
|
1630
1701
|
writer.line("val trimmedPath = path.trimStart('/')");
|
|
@@ -1721,7 +1792,7 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1721
1792
|
const contextRequired = contextFields.some((field) => !hasDefault(field));
|
|
1722
1793
|
const contextParam = contextFields.length > 0 ? `requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}, ` : "";
|
|
1723
1794
|
writer.blank();
|
|
1724
|
-
writer.block(`class ${clientName}(private val baseUrl: String, ${contextParam}private val client: OkHttpClient = OkHttpClient(), private val json: Json = Json { ignoreUnknownKeys = true }, private val requestInterceptor: (suspend (Request.Builder) -> Unit)? = null, private val responseInterceptor: (suspend (Request, Response) -> Unit)? = null, private val retry: RetryPolicy? = null)`, () => {
|
|
1795
|
+
writer.block(`class ${clientName}(private val baseUrl: String, ${contextParam}private val client: OkHttpClient = OkHttpClient(), private val json: Json = Json { ignoreUnknownKeys = true }, private val requestInterceptor: (suspend (Request.Builder) -> Unit)? = null, private val responseInterceptor: (suspend (Request, Response) -> Unit)? = null, private val retry: RetryPolicy? = null, private val reconnect: ReconnectPolicy? = null)`, () => {
|
|
1725
1796
|
if (contextFields.length > 0) {
|
|
1726
1797
|
writer.blank();
|
|
1727
1798
|
writer.line("/** Values sent as headers on every request, from the api's request context. */");
|
|
@@ -1746,6 +1817,20 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1746
1817
|
"val initialDelay: kotlin.time.Duration = 500.milliseconds",
|
|
1747
1818
|
"val maximumDelay: kotlin.time.Duration = 30.seconds"
|
|
1748
1819
|
]);
|
|
1820
|
+
writer.blank();
|
|
1821
|
+
writer.line("/**");
|
|
1822
|
+
writer.line(" * Reconnects an event stream on a GET, HEAD, OPTIONS, PUT or DELETE when its connection drops,");
|
|
1823
|
+
writer.line(" * sending the id of the last event as Last-Event-ID.");
|
|
1824
|
+
writer.line(" *");
|
|
1825
|
+
writer.line(" * @property maximumAttempts Reconnections in a row before the stream fails. An event received resets the count.");
|
|
1826
|
+
writer.line(" * @property reconnectionTime What to wait before reconnecting, until the server sends a retry field.");
|
|
1827
|
+
writer.line(" * @property maximumDelay What the wait may grow to after reconnections in a row that fail.");
|
|
1828
|
+
writer.line(" */");
|
|
1829
|
+
emitConstructorClass(writer, "data class ReconnectPolicy", [
|
|
1830
|
+
"val maximumAttempts: Int = 5",
|
|
1831
|
+
"val reconnectionTime: kotlin.time.Duration = 3.seconds",
|
|
1832
|
+
"val maximumDelay: kotlin.time.Duration = 30.seconds"
|
|
1833
|
+
]);
|
|
1749
1834
|
if (usesMultipart) {
|
|
1750
1835
|
writer.blank();
|
|
1751
1836
|
emitConstructorClass(writer, "data class MultipartFile", [
|
|
@@ -1801,7 +1886,7 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1801
1886
|
}
|
|
1802
1887
|
if (groups.length > 0) for (const group of groups) {
|
|
1803
1888
|
writer.blank();
|
|
1804
|
-
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor, retry)`);
|
|
1889
|
+
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor, retry, reconnect)`);
|
|
1805
1890
|
}
|
|
1806
1891
|
for (const method of flatMethods) {
|
|
1807
1892
|
writer.blank();
|
|
@@ -1476,6 +1476,35 @@ const emitErrorBranches = (writer, method, context) => {
|
|
|
1476
1476
|
}
|
|
1477
1477
|
writer.line(`else -> throw ${failureRef}.Unexpected(statusCode = statusCode, data = data)`);
|
|
1478
1478
|
};
|
|
1479
|
+
const IDEMPOTENT_METHODS = new Set([
|
|
1480
|
+
"GET",
|
|
1481
|
+
"HEAD",
|
|
1482
|
+
"OPTIONS",
|
|
1483
|
+
"PUT",
|
|
1484
|
+
"DELETE"
|
|
1485
|
+
]);
|
|
1486
|
+
/**
|
|
1487
|
+
* Writes the reconnection an event stream reopens through and returns the argument handing it to `Kizuna.events`, or
|
|
1488
|
+
* nothing for a method a client may not resend.
|
|
1489
|
+
*/
|
|
1490
|
+
const emitReopen = (writer, method, operationRef, status) => {
|
|
1491
|
+
if (!IDEMPOTENT_METHODS.has(method.method)) return "";
|
|
1492
|
+
closure(writer, "val reconnection = reconnect?.let { policy ->", () => {
|
|
1493
|
+
closure(writer, "Kizuna.Reconnection(policy) { lastEventId ->", () => {
|
|
1494
|
+
writer.line("val again = requestBuilder.build().newBuilder()");
|
|
1495
|
+
writer.line("if (lastEventId.isNotEmpty()) again.header(\"Last-Event-ID\", lastEventId)");
|
|
1496
|
+
writer.line("requestInterceptor?.invoke(again)");
|
|
1497
|
+
writer.line("val reopened = Kizuna.send(client, again.build(), retry)");
|
|
1498
|
+
writer.line("val eventStream = reopened.header(\"Content-Type\")?.lowercase()?.startsWith(\"text/event-stream\") == true");
|
|
1499
|
+
writer.block("when", () => {
|
|
1500
|
+
writer.line("reopened.code == 204 -> { reopened.close(); null }");
|
|
1501
|
+
writer.line(`reopened.code == ${status} && eventStream -> reopened`);
|
|
1502
|
+
writer.line(`else -> { reopened.close(); throw ${operationRef}.Failure.Unexpected(statusCode = reopened.code, data = ByteArray(0)) }`);
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1505
|
+
});
|
|
1506
|
+
return ", reconnection";
|
|
1507
|
+
};
|
|
1479
1508
|
const emitStreamMethodTail = (writer, method, context) => {
|
|
1480
1509
|
const operationRef = `${context.clientName}.${method.operationName}`;
|
|
1481
1510
|
const stream = method.stream;
|
|
@@ -1488,7 +1517,7 @@ const emitStreamMethodTail = (writer, method, context) => {
|
|
|
1488
1517
|
else if (stream.events) {
|
|
1489
1518
|
const events = stream.events;
|
|
1490
1519
|
const elementType = streamElementType(method, context, "client");
|
|
1491
|
-
closure(writer, `val body = Kizuna.events<${elementType}>(httpResponse) { event ->`, () => {
|
|
1520
|
+
closure(writer, `val body = Kizuna.events<${elementType}>(httpResponse${emitReopen(writer, method, operationRef, stream.status)}) { event ->`, () => {
|
|
1492
1521
|
writer.block("when (event.event)", () => {
|
|
1493
1522
|
for (const event of events) {
|
|
1494
1523
|
const payloadType = resolveType(event.type, method.operationName, context);
|
|
@@ -1499,7 +1528,8 @@ const emitStreamMethodTail = (writer, method, context) => {
|
|
|
1499
1528
|
});
|
|
1500
1529
|
} else {
|
|
1501
1530
|
const elementType = streamElementType(method, context, "client");
|
|
1502
|
-
|
|
1531
|
+
const reconnection = emitReopen(writer, method, operationRef, stream.status);
|
|
1532
|
+
writer.line(`val body = Kizuna.events<${elementType}>(httpResponse${reconnection}) { event -> json.decodeFromString<${elementType}>(event.data) }`);
|
|
1503
1533
|
}
|
|
1504
1534
|
if (hasHeaders) {
|
|
1505
1535
|
for (const field of successResponse.responseHeaders) writer.line(`val ${escapeKeyword(field.name)} = httpResponse.header(${stringLiteral(field.wireName)})`);
|
|
@@ -1545,7 +1575,7 @@ const emitSubClientMethod = (writer, method, context) => {
|
|
|
1545
1575
|
};
|
|
1546
1576
|
const emitSubClientClass = (writer, group, clientName, context) => {
|
|
1547
1577
|
writer.blank();
|
|
1548
|
-
writer.block(`class ${group.className}(private val client: OkHttpClient, private val baseUrl: String, private val json: Json, private val requestContextHeaders: Map<String, String>, private val requestInterceptor: (suspend (Request.Builder) -> Unit)?, private val responseInterceptor: (suspend (Request, Response) -> Unit)?, private val retry: ${clientName}.RetryPolicy?)`, () => {
|
|
1578
|
+
writer.block(`class ${group.className}(private val client: OkHttpClient, private val baseUrl: String, private val json: Json, private val requestContextHeaders: Map<String, String>, private val requestInterceptor: (suspend (Request.Builder) -> Unit)?, private val responseInterceptor: (suspend (Request, Response) -> Unit)?, private val retry: ${clientName}.RetryPolicy?, private val reconnect: ${clientName}.ReconnectPolicy?)`, () => {
|
|
1549
1579
|
for (const method of group.methods) {
|
|
1550
1580
|
writer.blank();
|
|
1551
1581
|
emitSubClientMethod(writer, method, context);
|
|
@@ -1557,10 +1587,12 @@ const closure = (writer, opener, body) => {
|
|
|
1557
1587
|
writer.indent(body);
|
|
1558
1588
|
writer.line("}");
|
|
1559
1589
|
};
|
|
1560
|
-
const emitKizunaStreaming = (writer) => {
|
|
1590
|
+
const emitKizunaStreaming = (writer, clientName) => {
|
|
1561
1591
|
writer.blank();
|
|
1562
1592
|
writer.line("data class ServerSentEvent(val event: String?, val data: String, val id: String?, val retry: Int?)");
|
|
1563
1593
|
writer.blank();
|
|
1594
|
+
writer.line(`class Reconnection(val policy: ${clientName}.ReconnectPolicy, val reopen: suspend (String) -> Response?)`);
|
|
1595
|
+
writer.blank();
|
|
1564
1596
|
writer.block("private suspend fun readLine(source: okio.BufferedSource): String?", () => {
|
|
1565
1597
|
writer.line("return kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { source.readUtf8Line() }");
|
|
1566
1598
|
});
|
|
@@ -1587,44 +1619,83 @@ const emitKizunaStreaming = (writer) => {
|
|
|
1587
1619
|
});
|
|
1588
1620
|
});
|
|
1589
1621
|
writer.blank();
|
|
1590
|
-
writer.
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
writer.line("val
|
|
1595
|
-
writer.
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
writer.
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1622
|
+
writer.line("private val LINE_ENDINGS = okio.ByteString.of('\\r'.code.toByte(), '\\n'.code.toByte())");
|
|
1623
|
+
writer.blank();
|
|
1624
|
+
writer.block("private suspend fun readEventLine(source: okio.BufferedSource): String?", () => {
|
|
1625
|
+
closure(writer, "return kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {", () => {
|
|
1626
|
+
writer.line("val end = source.indexOfElement(LINE_ENDINGS)");
|
|
1627
|
+
writer.block("if (end == -1L)", () => {
|
|
1628
|
+
writer.line("if (source.exhausted()) null else source.readUtf8()");
|
|
1629
|
+
});
|
|
1630
|
+
writer.block("else", () => {
|
|
1631
|
+
writer.line("val line = source.readUtf8(end)");
|
|
1632
|
+
writer.line("val carriageReturn = source.readByte() == '\\r'.code.toByte()");
|
|
1633
|
+
writer.line("if (carriageReturn && source.request(1) && source.buffer[0] == '\\n'.code.toByte()) source.skip(1)");
|
|
1634
|
+
writer.line("line");
|
|
1635
|
+
});
|
|
1636
|
+
});
|
|
1637
|
+
});
|
|
1638
|
+
writer.blank();
|
|
1639
|
+
writer.block("fun serverSentEvents(response: Response, reconnect: Reconnection?): Flow<ServerSentEvent> = flow", () => {
|
|
1640
|
+
writer.line("var lastEventId = \"\"");
|
|
1641
|
+
writer.line("var reconnectionTime = reconnect?.policy?.reconnectionTime ?: 3.seconds");
|
|
1642
|
+
writer.line("var current: Response? = response");
|
|
1643
|
+
writer.line("var attempts = 0");
|
|
1644
|
+
writer.line("var failure: Throwable? = null");
|
|
1645
|
+
writer.block("while (true)", () => {
|
|
1646
|
+
closure(writer, "current?.use { open ->", () => {
|
|
1647
|
+
writer.line("val source = open.body?.source() ?: return@use");
|
|
1648
|
+
writer.line("var eventType = \"\"");
|
|
1649
|
+
writer.line("val dataLines = mutableListOf<String>()");
|
|
1650
|
+
writer.line("var pendingId = lastEventId");
|
|
1651
|
+
writer.line("var retry: Int? = null");
|
|
1652
|
+
writer.line("var firstLine = true");
|
|
1653
|
+
writer.block("while (true)", () => {
|
|
1654
|
+
writer.line("val received = try { readEventLine(source) } catch (error: java.io.IOException) { failure = error; null } ?: break");
|
|
1655
|
+
writer.line("val line = if (firstLine) received.removePrefix(\"\\uFEFF\") else received");
|
|
1656
|
+
writer.line("firstLine = false");
|
|
1657
|
+
writer.block("if (line.isEmpty())", () => {
|
|
1658
|
+
writer.line("lastEventId = pendingId");
|
|
1659
|
+
writer.block("if (dataLines.isNotEmpty())", () => {
|
|
1660
|
+
writer.line("emit(ServerSentEvent(eventType.ifEmpty { null }, dataLines.joinToString(\"\\n\"), lastEventId.ifEmpty { null }, retry))");
|
|
1661
|
+
writer.line("attempts = 0");
|
|
1662
|
+
writer.line("failure = null");
|
|
1663
|
+
writer.line("dataLines.clear()");
|
|
1664
|
+
writer.line("retry = null");
|
|
1665
|
+
});
|
|
1666
|
+
writer.line("eventType = \"\"");
|
|
1667
|
+
writer.line("continue");
|
|
1668
|
+
});
|
|
1669
|
+
writer.line("if (line.startsWith(\":\")) continue");
|
|
1670
|
+
writer.line("val separator = line.indexOf(':')");
|
|
1671
|
+
writer.line("val field = if (separator == -1) line else line.substring(0, separator)");
|
|
1672
|
+
writer.line("val value = (if (separator == -1) \"\" else line.substring(separator + 1)).removePrefix(\" \")");
|
|
1673
|
+
writer.block("when (field)", () => {
|
|
1674
|
+
writer.line("\"event\" -> eventType = value");
|
|
1675
|
+
writer.line("\"data\" -> dataLines.add(value)");
|
|
1676
|
+
writer.line("\"id\" -> if (!value.contains('\\u0000')) pendingId = value");
|
|
1677
|
+
writer.line("\"retry\" -> if (value.isNotEmpty() && value.all { character -> character in '0'..'9' }) value.toIntOrNull()?.let { milliseconds -> retry = milliseconds; reconnectionTime = minOf(milliseconds.milliseconds, 3_600_000.milliseconds) }");
|
|
1604
1678
|
});
|
|
1605
|
-
writer.line("eventType = \"\"");
|
|
1606
|
-
writer.line("continue");
|
|
1607
|
-
});
|
|
1608
|
-
writer.line("if (line.startsWith(\":\")) continue");
|
|
1609
|
-
writer.line("val separator = line.indexOf(':')");
|
|
1610
|
-
writer.line("val field = if (separator == -1) line else line.substring(0, separator)");
|
|
1611
|
-
writer.line("val value = (if (separator == -1) \"\" else line.substring(separator + 1)).removePrefix(\" \")");
|
|
1612
|
-
writer.block("when (field)", () => {
|
|
1613
|
-
writer.line("\"event\" -> eventType = value");
|
|
1614
|
-
writer.line("\"data\" -> dataLines.add(value)");
|
|
1615
|
-
writer.line("\"id\" -> if (!value.contains('\\u0000')) lastEventId = value");
|
|
1616
|
-
writer.line("\"retry\" -> if (value.isNotEmpty() && value.all { it.isDigit() }) retry = value.toIntOrNull()");
|
|
1617
1679
|
});
|
|
1618
1680
|
});
|
|
1681
|
+
writer.block("if (reconnect == null || attempts >= reconnect.policy.maximumAttempts)", () => {
|
|
1682
|
+
writer.line("failure?.let { throw it }");
|
|
1683
|
+
writer.line("return@flow");
|
|
1684
|
+
});
|
|
1685
|
+
writer.line("attempts += 1");
|
|
1686
|
+
writer.line("val ceiling = minOf(reconnect.policy.maximumDelay, reconnectionTime * Math.pow(2.0, (attempts - 1).toDouble()))");
|
|
1687
|
+
writer.line("val extra = (ceiling - reconnectionTime).coerceAtLeast(kotlin.time.Duration.ZERO)");
|
|
1688
|
+
writer.line("kotlinx.coroutines.delay(reconnectionTime + extra * Math.random())");
|
|
1689
|
+
writer.line("current = try { reconnect.reopen(lastEventId) ?: return@flow } catch (error: java.io.IOException) { failure = error; null }");
|
|
1619
1690
|
});
|
|
1620
1691
|
});
|
|
1621
1692
|
writer.blank();
|
|
1622
|
-
writer.line("fun <Event : Any> events(response: Response, decode: (ServerSentEvent) -> Event?): Flow<Event> = serverSentEvents(response).mapNotNull(decode)");
|
|
1693
|
+
writer.line("fun <Event : Any> events(response: Response, reconnect: Reconnection? = null, decode: (ServerSentEvent) -> Event?): Flow<Event> = serverSentEvents(response, reconnect).mapNotNull(decode)");
|
|
1623
1694
|
};
|
|
1624
1695
|
const emitKizunaObject = (writer, options) => {
|
|
1625
1696
|
writer.blank();
|
|
1626
1697
|
writer.block("private object Kizuna", () => {
|
|
1627
|
-
if (options.streaming) emitKizunaStreaming(writer);
|
|
1698
|
+
if (options.streaming) emitKizunaStreaming(writer, options.clientName);
|
|
1628
1699
|
writer.block("fun resolveUrl(baseUrl: String, path: String): HttpUrl.Builder", () => {
|
|
1629
1700
|
writer.line("val base = baseUrl.toHttpUrl()");
|
|
1630
1701
|
writer.line("val trimmedPath = path.trimStart('/')");
|
|
@@ -1721,7 +1792,7 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1721
1792
|
const contextRequired = contextFields.some((field) => !hasDefault(field));
|
|
1722
1793
|
const contextParam = contextFields.length > 0 ? `requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}, ` : "";
|
|
1723
1794
|
writer.blank();
|
|
1724
|
-
writer.block(`class ${clientName}(private val baseUrl: String, ${contextParam}private val client: OkHttpClient = OkHttpClient(), private val json: Json = Json { ignoreUnknownKeys = true }, private val requestInterceptor: (suspend (Request.Builder) -> Unit)? = null, private val responseInterceptor: (suspend (Request, Response) -> Unit)? = null, private val retry: RetryPolicy? = null)`, () => {
|
|
1795
|
+
writer.block(`class ${clientName}(private val baseUrl: String, ${contextParam}private val client: OkHttpClient = OkHttpClient(), private val json: Json = Json { ignoreUnknownKeys = true }, private val requestInterceptor: (suspend (Request.Builder) -> Unit)? = null, private val responseInterceptor: (suspend (Request, Response) -> Unit)? = null, private val retry: RetryPolicy? = null, private val reconnect: ReconnectPolicy? = null)`, () => {
|
|
1725
1796
|
if (contextFields.length > 0) {
|
|
1726
1797
|
writer.blank();
|
|
1727
1798
|
writer.line("/** Values sent as headers on every request, from the api's request context. */");
|
|
@@ -1746,6 +1817,20 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1746
1817
|
"val initialDelay: kotlin.time.Duration = 500.milliseconds",
|
|
1747
1818
|
"val maximumDelay: kotlin.time.Duration = 30.seconds"
|
|
1748
1819
|
]);
|
|
1820
|
+
writer.blank();
|
|
1821
|
+
writer.line("/**");
|
|
1822
|
+
writer.line(" * Reconnects an event stream on a GET, HEAD, OPTIONS, PUT or DELETE when its connection drops,");
|
|
1823
|
+
writer.line(" * sending the id of the last event as Last-Event-ID.");
|
|
1824
|
+
writer.line(" *");
|
|
1825
|
+
writer.line(" * @property maximumAttempts Reconnections in a row before the stream fails. An event received resets the count.");
|
|
1826
|
+
writer.line(" * @property reconnectionTime What to wait before reconnecting, until the server sends a retry field.");
|
|
1827
|
+
writer.line(" * @property maximumDelay What the wait may grow to after reconnections in a row that fail.");
|
|
1828
|
+
writer.line(" */");
|
|
1829
|
+
emitConstructorClass(writer, "data class ReconnectPolicy", [
|
|
1830
|
+
"val maximumAttempts: Int = 5",
|
|
1831
|
+
"val reconnectionTime: kotlin.time.Duration = 3.seconds",
|
|
1832
|
+
"val maximumDelay: kotlin.time.Duration = 30.seconds"
|
|
1833
|
+
]);
|
|
1749
1834
|
if (usesMultipart) {
|
|
1750
1835
|
writer.blank();
|
|
1751
1836
|
emitConstructorClass(writer, "data class MultipartFile", [
|
|
@@ -1801,7 +1886,7 @@ const emitClient = (writer, config, partition, context, typesByOperation, regist
|
|
|
1801
1886
|
}
|
|
1802
1887
|
if (groups.length > 0) for (const group of groups) {
|
|
1803
1888
|
writer.blank();
|
|
1804
|
-
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor, retry)`);
|
|
1889
|
+
writer.line(`val ${escapeKeyword(group.propertyName)} = ${group.className}(client, baseUrl, json, ${contextFields.length > 0 ? "requestContextHeaders" : "emptyMap()"}, requestInterceptor, responseInterceptor, retry, reconnect)`);
|
|
1805
1890
|
}
|
|
1806
1891
|
for (const method of flatMethods) {
|
|
1807
1892
|
writer.blank();
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_generator = require("./generator-
|
|
2
|
+
const require_generator = require("./generator-Dft2DL-3.cjs");
|
|
3
3
|
let kizunajs_generator = require("kizunajs/generator");
|
|
4
4
|
let zod = require("zod");
|
|
5
5
|
//#region src/client-target.ts
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as kotlinClientWalk, t as generateKotlinClient } from "./generator-
|
|
1
|
+
import { n as kotlinClientWalk, t as generateKotlinClient } from "./generator-DHi8XSWb.mjs";
|
|
2
2
|
import { defineClient } from "kizunajs/generator";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kizunajs/kotlin",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.21",
|
|
4
4
|
"description": "Kotlin client generation for Kizuna.js APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kizunajs",
|
|
@@ -60,14 +60,14 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@kizunajs/cli": "2.0.0-beta.
|
|
63
|
+
"@kizunajs/cli": "2.0.0-beta.21"
|
|
64
64
|
},
|
|
65
65
|
"peerDependencies": {
|
|
66
|
-
"kizunajs": "2.0.0-beta.
|
|
66
|
+
"kizunajs": "2.0.0-beta.21",
|
|
67
67
|
"zod": "^4.0.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"kizunajs": "2.0.0-beta.
|
|
70
|
+
"kizunajs": "2.0.0-beta.21",
|
|
71
71
|
"@types/node": "^22.9.0",
|
|
72
72
|
"tsdown": "^0.21.0",
|
|
73
73
|
"typescript": "^6.0.3",
|