@kizunajs/swift 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 generateSwiftClient } from "./generator-
|
|
2
|
+
import { t as generateSwiftClient } from "./generator-BQDx47ez.mjs";
|
|
3
3
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
@@ -1341,6 +1341,11 @@ const emitKizunaStreaming = (writer, clientName) => {
|
|
|
1341
1341
|
writer.line("let retry: Int?");
|
|
1342
1342
|
});
|
|
1343
1343
|
writer.blank();
|
|
1344
|
+
writer.block("struct Reconnection: Sendable", () => {
|
|
1345
|
+
writer.line(`let policy: ${clientName}.ReconnectPolicy`);
|
|
1346
|
+
writer.line("let reopen: @Sendable (String) async throws -> URLSession.AsyncBytes?");
|
|
1347
|
+
});
|
|
1348
|
+
writer.blank();
|
|
1344
1349
|
writer.block(`static func open<Failure: KizunaFailure>(_ request: inout URLRequest, session: URLSession, retry: ${clientName}.RetryPolicy?, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)?, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)?, failure: Failure.Type) async throws(Failure) -> (URLSession.AsyncBytes, Int, HTTPURLResponse)`, () => {
|
|
1345
1350
|
writer.line("if let requestMiddleware {");
|
|
1346
1351
|
writer.line(" do { try await requestMiddleware(&request) }");
|
|
@@ -1433,56 +1438,97 @@ const emitKizunaStreaming = (writer, clientName) => {
|
|
|
1433
1438
|
});
|
|
1434
1439
|
});
|
|
1435
1440
|
writer.blank();
|
|
1436
|
-
writer.block("static func serverSentEvents(_
|
|
1441
|
+
writer.block("static func serverSentEvents(_ first: URLSession.AsyncBytes, reconnect: Reconnection?) -> AsyncThrowingStream<ServerSentEvent, Swift.Error>", () => {
|
|
1437
1442
|
closure(writer, "AsyncThrowingStream { continuation in", () => {
|
|
1438
1443
|
writer.block("let task = Task", () => {
|
|
1439
|
-
writer.line("var eventType = \"\"");
|
|
1440
|
-
writer.line("var dataLines: [String] = []");
|
|
1441
1444
|
writer.line("var lastEventId = \"\"");
|
|
1442
|
-
writer.line("var
|
|
1443
|
-
writer.
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
writer.line("
|
|
1449
|
-
writer.line("
|
|
1450
|
-
writer.line("
|
|
1451
|
-
writer.line("
|
|
1452
|
-
writer.line("
|
|
1453
|
-
writer.
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1445
|
+
writer.line("var reconnectionTime = reconnect?.policy.reconnectionTime ?? 3");
|
|
1446
|
+
writer.line("var bytes: URLSession.AsyncBytes? = first");
|
|
1447
|
+
writer.line("var attempts = 0");
|
|
1448
|
+
writer.line("var failure: Swift.Error? = nil");
|
|
1449
|
+
writer.block("while true", () => {
|
|
1450
|
+
writer.block("if let current = bytes", () => {
|
|
1451
|
+
writer.line("var eventType = \"\"");
|
|
1452
|
+
writer.line("var dataLines: [String] = []");
|
|
1453
|
+
writer.line("var pendingId = lastEventId");
|
|
1454
|
+
writer.line("var retry: Int? = nil");
|
|
1455
|
+
writer.line("var firstLine = true");
|
|
1456
|
+
writer.block("do", () => {
|
|
1457
|
+
writer.block("for try await received in lines(current)", () => {
|
|
1458
|
+
writer.line("let line = firstLine && received.hasPrefix(\"\\u{FEFF}\") ? String(received.dropFirst()) : received");
|
|
1459
|
+
writer.line("firstLine = false");
|
|
1460
|
+
writer.line("if line.isEmpty {");
|
|
1461
|
+
writer.line(" lastEventId = pendingId");
|
|
1462
|
+
writer.line(" if !dataLines.isEmpty {");
|
|
1463
|
+
writer.line(" continuation.yield(ServerSentEvent(event: eventType.isEmpty ? nil : eventType, data: dataLines.joined(separator: \"\\n\"), id: lastEventId.isEmpty ? nil : lastEventId, retry: retry))");
|
|
1464
|
+
writer.line(" attempts = 0");
|
|
1465
|
+
writer.line(" failure = nil");
|
|
1466
|
+
writer.line(" dataLines = []");
|
|
1467
|
+
writer.line(" retry = nil");
|
|
1468
|
+
writer.line(" }");
|
|
1469
|
+
writer.line(" eventType = \"\"");
|
|
1470
|
+
writer.line(" continue");
|
|
1471
|
+
writer.line("}");
|
|
1472
|
+
writer.line("if line.hasPrefix(\":\") { continue }");
|
|
1473
|
+
writer.line("let field: Substring");
|
|
1474
|
+
writer.line("var value: Substring");
|
|
1475
|
+
writer.line("if let separator = line.firstIndex(of: \":\") {");
|
|
1476
|
+
writer.line(" field = line[..<separator]");
|
|
1477
|
+
writer.line(" value = line[line.index(after: separator)...]");
|
|
1478
|
+
writer.line(" if value.hasPrefix(\" \") { value = value.dropFirst() }");
|
|
1479
|
+
writer.line("} else {");
|
|
1480
|
+
writer.line(" field = Substring(line)");
|
|
1481
|
+
writer.line(" value = \"\"");
|
|
1482
|
+
writer.line("}");
|
|
1483
|
+
writer.line("switch field {");
|
|
1484
|
+
writer.line("case \"event\": eventType = String(value)");
|
|
1485
|
+
writer.line("case \"data\": dataLines.append(String(value))");
|
|
1486
|
+
writer.line("case \"id\": if !value.contains(\"\\0\") { pendingId = String(value) }");
|
|
1487
|
+
writer.line("case \"retry\": if !value.isEmpty, value.allSatisfy({ $0.isASCII && $0.isNumber }), let milliseconds = Int(value) { retry = milliseconds; reconnectionTime = min(TimeInterval(milliseconds) / 1000, 3600) }");
|
|
1488
|
+
writer.line("default: break");
|
|
1489
|
+
writer.line("}");
|
|
1490
|
+
});
|
|
1491
|
+
});
|
|
1492
|
+
writer.line("catch { failure = error }");
|
|
1493
|
+
});
|
|
1494
|
+
writer.line("guard let reconnect, !Task.isCancelled, attempts < reconnect.policy.maximumAttempts else {");
|
|
1495
|
+
writer.line(" continuation.finish(throwing: failure)");
|
|
1496
|
+
writer.line(" return");
|
|
1497
|
+
writer.line("}");
|
|
1498
|
+
writer.line("attempts += 1");
|
|
1499
|
+
writer.line("let ceiling = min(reconnect.policy.maximumDelay, reconnectionTime * pow(2, Double(attempts - 1)))");
|
|
1500
|
+
writer.line("let delay = max(0, reconnectionTime) + Double.random(in: 0...max(0, ceiling - reconnectionTime))");
|
|
1501
|
+
writer.line("do { try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) }");
|
|
1502
|
+
writer.line("catch {");
|
|
1503
|
+
writer.line(" continuation.finish(throwing: failure)");
|
|
1504
|
+
writer.line(" return");
|
|
1505
|
+
writer.line("}");
|
|
1506
|
+
writer.block("do", () => {
|
|
1507
|
+
writer.line("guard let reopened = try await reconnect.reopen(lastEventId) else {");
|
|
1508
|
+
writer.line(" continuation.finish()");
|
|
1509
|
+
writer.line(" return");
|
|
1471
1510
|
writer.line("}");
|
|
1511
|
+
writer.line("bytes = reopened");
|
|
1512
|
+
});
|
|
1513
|
+
writer.block("catch let error as URLError", () => {
|
|
1514
|
+
writer.line("failure = error");
|
|
1515
|
+
writer.line("bytes = nil");
|
|
1516
|
+
});
|
|
1517
|
+
writer.block("catch", () => {
|
|
1518
|
+
writer.line("continuation.finish(throwing: error)");
|
|
1519
|
+
writer.line("return");
|
|
1472
1520
|
});
|
|
1473
|
-
writer.line("continuation.finish()");
|
|
1474
1521
|
});
|
|
1475
|
-
writer.line("catch { continuation.finish(throwing: error) }");
|
|
1476
1522
|
});
|
|
1477
1523
|
writer.line("continuation.onTermination = { _ in task.cancel() }");
|
|
1478
1524
|
});
|
|
1479
1525
|
});
|
|
1480
1526
|
writer.blank();
|
|
1481
|
-
writer.block("static func events<Event: Sendable>(_ bytes: URLSession.AsyncBytes, using decoder: JSONDecoder, _ decode: @escaping @Sendable (ServerSentEvent, JSONDecoder) throws -> Event?) -> AsyncThrowingStream<Event, Swift.Error>", () => {
|
|
1527
|
+
writer.block("static func events<Event: Sendable>(_ bytes: URLSession.AsyncBytes, using decoder: JSONDecoder, reconnect: Reconnection? = nil, _ decode: @escaping @Sendable (ServerSentEvent, JSONDecoder) throws -> Event?) -> AsyncThrowingStream<Event, Swift.Error>", () => {
|
|
1482
1528
|
closure(writer, "AsyncThrowingStream { continuation in", () => {
|
|
1483
1529
|
writer.block("let task = Task", () => {
|
|
1484
1530
|
writer.block("do", () => {
|
|
1485
|
-
writer.block("for try await event in serverSentEvents(bytes)", () => {
|
|
1531
|
+
writer.block("for try await event in serverSentEvents(bytes, reconnect: reconnect)", () => {
|
|
1486
1532
|
writer.line("if let decoded = try decode(event, decoder) { continuation.yield(decoded) }");
|
|
1487
1533
|
});
|
|
1488
1534
|
writer.line("continuation.finish()");
|
|
@@ -1865,6 +1911,35 @@ const emitErrorCases = (writer, method, context, receiver, prelude) => {
|
|
|
1865
1911
|
}
|
|
1866
1912
|
}
|
|
1867
1913
|
};
|
|
1914
|
+
const IDEMPOTENT_METHODS = new Set([
|
|
1915
|
+
"GET",
|
|
1916
|
+
"HEAD",
|
|
1917
|
+
"OPTIONS",
|
|
1918
|
+
"PUT",
|
|
1919
|
+
"DELETE"
|
|
1920
|
+
]);
|
|
1921
|
+
/**
|
|
1922
|
+
* Writes the closure that reopens an event stream and returns the argument handing it to `Kizuna.events`, or nothing
|
|
1923
|
+
* for a method a client may not resend.
|
|
1924
|
+
*/
|
|
1925
|
+
const emitReopen = (writer, method, receiver, failure, status) => {
|
|
1926
|
+
if (!IDEMPOTENT_METHODS.has(method.method)) return "";
|
|
1927
|
+
closure(writer, `let reopen: @Sendable (String) async throws -> URLSession.AsyncBytes? = { [request, session = ${receiver}session, retry = ${receiver}retry, requestMiddleware = ${receiver}requestMiddleware, responseMiddleware = ${receiver}responseMiddleware] lastEventId in`, () => {
|
|
1928
|
+
writer.line("var again = request");
|
|
1929
|
+
writer.line("if !lastEventId.isEmpty { again.setValue(lastEventId, forHTTPHeaderField: \"Last-Event-ID\") }");
|
|
1930
|
+
writer.line("let reopened: (URLSession.AsyncBytes, Int, HTTPURLResponse)");
|
|
1931
|
+
writer.line(`do { reopened = try await Kizuna.open(&again, session: session, retry: retry, requestMiddleware: requestMiddleware, responseMiddleware: responseMiddleware, failure: ${failure}.self) }`);
|
|
1932
|
+
writer.line(`catch let failure as ${failure} {`);
|
|
1933
|
+
writer.line(" if case .requestFailed(let underlying) = failure { throw underlying }");
|
|
1934
|
+
writer.line(" throw failure");
|
|
1935
|
+
writer.line("}");
|
|
1936
|
+
writer.line("if reopened.1 == 204 { return nil }");
|
|
1937
|
+
writer.line(`guard reopened.1 == ${status} else { throw ${failure}.unexpectedStatus(reopened.1, Foundation.Data()) }`);
|
|
1938
|
+
writer.line(`guard reopened.2.value(forHTTPHeaderField: "Content-Type")?.lowercased().hasPrefix("text/event-stream") == true else { throw ${failure}.invalidResponse }`);
|
|
1939
|
+
writer.line("return reopened.0");
|
|
1940
|
+
});
|
|
1941
|
+
return `, reconnect: ${receiver}reconnect.map { Kizuna.Reconnection(policy: $0, reopen: reopen) }`;
|
|
1942
|
+
};
|
|
1868
1943
|
const emitStreamMethodTail = (writer, method, context, receiver, responseBinding) => {
|
|
1869
1944
|
const failure = failureRef(method, context);
|
|
1870
1945
|
const stream = method.stream;
|
|
@@ -1880,7 +1955,8 @@ const emitStreamMethodTail = (writer, method, context, receiver, responseBinding
|
|
|
1880
1955
|
else if (stream.mode === "binary") writer.line("let body = Kizuna.chunks(bytes)");
|
|
1881
1956
|
else if (stream.events) {
|
|
1882
1957
|
const events = stream.events;
|
|
1883
|
-
|
|
1958
|
+
const elementType = streamElementType(method, context, "actor");
|
|
1959
|
+
closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder${emitReopen(writer, method, receiver, failure, stream.status)}) { event, decoder -> ${elementType}? in`, () => {
|
|
1884
1960
|
writer.line("switch event.event {");
|
|
1885
1961
|
for (const event of events) {
|
|
1886
1962
|
const payloadType = resolveType(event.type, method.operationName, context);
|
|
@@ -1891,7 +1967,7 @@ const emitStreamMethodTail = (writer, method, context, receiver, responseBinding
|
|
|
1891
1967
|
});
|
|
1892
1968
|
} else {
|
|
1893
1969
|
const elementType = streamElementType(method, context, "actor");
|
|
1894
|
-
closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder) { event, decoder -> ${elementType}? in`, () => {
|
|
1970
|
+
closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder${emitReopen(writer, method, receiver, failure, stream.status)}) { event, decoder -> ${elementType}? in`, () => {
|
|
1895
1971
|
writer.line(`try decoder.decode(${elementType}.self, from: Foundation.Data(event.data.utf8))`);
|
|
1896
1972
|
});
|
|
1897
1973
|
}
|
|
@@ -2022,6 +2098,22 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
|
|
|
2022
2098
|
writer.line("self.maximumDelay = maximumDelay");
|
|
2023
2099
|
});
|
|
2024
2100
|
});
|
|
2101
|
+
writer.blank();
|
|
2102
|
+
writer.docComment("Reconnects an event stream on a GET, HEAD, OPTIONS, PUT or DELETE when its connection drops, sending the id of the last event as Last-Event-ID.");
|
|
2103
|
+
writer.block("public struct ReconnectPolicy: Sendable", () => {
|
|
2104
|
+
writer.docComment("Reconnections in a row before the stream fails. An event received resets the count.");
|
|
2105
|
+
writer.line("public var maximumAttempts: Int");
|
|
2106
|
+
writer.docComment("Seconds to wait before reconnecting, until the server sends a retry field.");
|
|
2107
|
+
writer.line("public var reconnectionTime: TimeInterval");
|
|
2108
|
+
writer.docComment("Seconds the wait may grow to after reconnections in a row that fail.");
|
|
2109
|
+
writer.line("public var maximumDelay: TimeInterval");
|
|
2110
|
+
writer.blank();
|
|
2111
|
+
writer.block("public init(maximumAttempts: Int = 5, reconnectionTime: TimeInterval = 3, maximumDelay: TimeInterval = 30)", () => {
|
|
2112
|
+
writer.line("self.maximumAttempts = maximumAttempts");
|
|
2113
|
+
writer.line("self.reconnectionTime = reconnectionTime");
|
|
2114
|
+
writer.line("self.maximumDelay = maximumDelay");
|
|
2115
|
+
});
|
|
2116
|
+
});
|
|
2025
2117
|
if (usesMultipart) {
|
|
2026
2118
|
writer.blank();
|
|
2027
2119
|
writer.block("public struct MultipartFile: Sendable, Equatable", () => {
|
|
@@ -2120,15 +2212,17 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
|
|
|
2120
2212
|
writer.line("public let requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)?");
|
|
2121
2213
|
writer.line("public let responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)?");
|
|
2122
2214
|
writer.line("public let retry: RetryPolicy?");
|
|
2215
|
+
writer.line("public let reconnect: ReconnectPolicy?");
|
|
2123
2216
|
writer.line("let encoder: JSONEncoder");
|
|
2124
2217
|
writer.line("let decoder: JSONDecoder");
|
|
2125
2218
|
writer.blank();
|
|
2126
2219
|
const contextInitParam = contextFields.length > 0 ? `, requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}` : "";
|
|
2127
|
-
writer.block(`public init(baseURL: URL, session: URLSession = .shared, timeout: TimeInterval = 30, retry: RetryPolicy? = nil${contextInitParam}, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)? = nil, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)? = nil)`, () => {
|
|
2220
|
+
writer.block(`public init(baseURL: URL, session: URLSession = .shared, timeout: TimeInterval = 30, retry: RetryPolicy? = nil, reconnect: ReconnectPolicy? = nil${contextInitParam}, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)? = nil, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)? = nil)`, () => {
|
|
2128
2221
|
writer.line("self.baseURL = baseURL");
|
|
2129
2222
|
writer.line("self.session = session");
|
|
2130
2223
|
writer.line("self.timeout = timeout");
|
|
2131
2224
|
writer.line("self.retry = retry");
|
|
2225
|
+
writer.line("self.reconnect = reconnect");
|
|
2132
2226
|
if (contextFields.length > 0) writer.line("self.requestContextHeaders = requestContext.headerFields");
|
|
2133
2227
|
writer.line("self.requestMiddleware = requestMiddleware");
|
|
2134
2228
|
writer.line("self.responseMiddleware = responseMiddleware");
|
|
@@ -1341,6 +1341,11 @@ const emitKizunaStreaming = (writer, clientName) => {
|
|
|
1341
1341
|
writer.line("let retry: Int?");
|
|
1342
1342
|
});
|
|
1343
1343
|
writer.blank();
|
|
1344
|
+
writer.block("struct Reconnection: Sendable", () => {
|
|
1345
|
+
writer.line(`let policy: ${clientName}.ReconnectPolicy`);
|
|
1346
|
+
writer.line("let reopen: @Sendable (String) async throws -> URLSession.AsyncBytes?");
|
|
1347
|
+
});
|
|
1348
|
+
writer.blank();
|
|
1344
1349
|
writer.block(`static func open<Failure: KizunaFailure>(_ request: inout URLRequest, session: URLSession, retry: ${clientName}.RetryPolicy?, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)?, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)?, failure: Failure.Type) async throws(Failure) -> (URLSession.AsyncBytes, Int, HTTPURLResponse)`, () => {
|
|
1345
1350
|
writer.line("if let requestMiddleware {");
|
|
1346
1351
|
writer.line(" do { try await requestMiddleware(&request) }");
|
|
@@ -1433,56 +1438,97 @@ const emitKizunaStreaming = (writer, clientName) => {
|
|
|
1433
1438
|
});
|
|
1434
1439
|
});
|
|
1435
1440
|
writer.blank();
|
|
1436
|
-
writer.block("static func serverSentEvents(_
|
|
1441
|
+
writer.block("static func serverSentEvents(_ first: URLSession.AsyncBytes, reconnect: Reconnection?) -> AsyncThrowingStream<ServerSentEvent, Swift.Error>", () => {
|
|
1437
1442
|
closure(writer, "AsyncThrowingStream { continuation in", () => {
|
|
1438
1443
|
writer.block("let task = Task", () => {
|
|
1439
|
-
writer.line("var eventType = \"\"");
|
|
1440
|
-
writer.line("var dataLines: [String] = []");
|
|
1441
1444
|
writer.line("var lastEventId = \"\"");
|
|
1442
|
-
writer.line("var
|
|
1443
|
-
writer.
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
writer.line("
|
|
1449
|
-
writer.line("
|
|
1450
|
-
writer.line("
|
|
1451
|
-
writer.line("
|
|
1452
|
-
writer.line("
|
|
1453
|
-
writer.
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1445
|
+
writer.line("var reconnectionTime = reconnect?.policy.reconnectionTime ?? 3");
|
|
1446
|
+
writer.line("var bytes: URLSession.AsyncBytes? = first");
|
|
1447
|
+
writer.line("var attempts = 0");
|
|
1448
|
+
writer.line("var failure: Swift.Error? = nil");
|
|
1449
|
+
writer.block("while true", () => {
|
|
1450
|
+
writer.block("if let current = bytes", () => {
|
|
1451
|
+
writer.line("var eventType = \"\"");
|
|
1452
|
+
writer.line("var dataLines: [String] = []");
|
|
1453
|
+
writer.line("var pendingId = lastEventId");
|
|
1454
|
+
writer.line("var retry: Int? = nil");
|
|
1455
|
+
writer.line("var firstLine = true");
|
|
1456
|
+
writer.block("do", () => {
|
|
1457
|
+
writer.block("for try await received in lines(current)", () => {
|
|
1458
|
+
writer.line("let line = firstLine && received.hasPrefix(\"\\u{FEFF}\") ? String(received.dropFirst()) : received");
|
|
1459
|
+
writer.line("firstLine = false");
|
|
1460
|
+
writer.line("if line.isEmpty {");
|
|
1461
|
+
writer.line(" lastEventId = pendingId");
|
|
1462
|
+
writer.line(" if !dataLines.isEmpty {");
|
|
1463
|
+
writer.line(" continuation.yield(ServerSentEvent(event: eventType.isEmpty ? nil : eventType, data: dataLines.joined(separator: \"\\n\"), id: lastEventId.isEmpty ? nil : lastEventId, retry: retry))");
|
|
1464
|
+
writer.line(" attempts = 0");
|
|
1465
|
+
writer.line(" failure = nil");
|
|
1466
|
+
writer.line(" dataLines = []");
|
|
1467
|
+
writer.line(" retry = nil");
|
|
1468
|
+
writer.line(" }");
|
|
1469
|
+
writer.line(" eventType = \"\"");
|
|
1470
|
+
writer.line(" continue");
|
|
1471
|
+
writer.line("}");
|
|
1472
|
+
writer.line("if line.hasPrefix(\":\") { continue }");
|
|
1473
|
+
writer.line("let field: Substring");
|
|
1474
|
+
writer.line("var value: Substring");
|
|
1475
|
+
writer.line("if let separator = line.firstIndex(of: \":\") {");
|
|
1476
|
+
writer.line(" field = line[..<separator]");
|
|
1477
|
+
writer.line(" value = line[line.index(after: separator)...]");
|
|
1478
|
+
writer.line(" if value.hasPrefix(\" \") { value = value.dropFirst() }");
|
|
1479
|
+
writer.line("} else {");
|
|
1480
|
+
writer.line(" field = Substring(line)");
|
|
1481
|
+
writer.line(" value = \"\"");
|
|
1482
|
+
writer.line("}");
|
|
1483
|
+
writer.line("switch field {");
|
|
1484
|
+
writer.line("case \"event\": eventType = String(value)");
|
|
1485
|
+
writer.line("case \"data\": dataLines.append(String(value))");
|
|
1486
|
+
writer.line("case \"id\": if !value.contains(\"\\0\") { pendingId = String(value) }");
|
|
1487
|
+
writer.line("case \"retry\": if !value.isEmpty, value.allSatisfy({ $0.isASCII && $0.isNumber }), let milliseconds = Int(value) { retry = milliseconds; reconnectionTime = min(TimeInterval(milliseconds) / 1000, 3600) }");
|
|
1488
|
+
writer.line("default: break");
|
|
1489
|
+
writer.line("}");
|
|
1490
|
+
});
|
|
1491
|
+
});
|
|
1492
|
+
writer.line("catch { failure = error }");
|
|
1493
|
+
});
|
|
1494
|
+
writer.line("guard let reconnect, !Task.isCancelled, attempts < reconnect.policy.maximumAttempts else {");
|
|
1495
|
+
writer.line(" continuation.finish(throwing: failure)");
|
|
1496
|
+
writer.line(" return");
|
|
1497
|
+
writer.line("}");
|
|
1498
|
+
writer.line("attempts += 1");
|
|
1499
|
+
writer.line("let ceiling = min(reconnect.policy.maximumDelay, reconnectionTime * pow(2, Double(attempts - 1)))");
|
|
1500
|
+
writer.line("let delay = max(0, reconnectionTime) + Double.random(in: 0...max(0, ceiling - reconnectionTime))");
|
|
1501
|
+
writer.line("do { try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000)) }");
|
|
1502
|
+
writer.line("catch {");
|
|
1503
|
+
writer.line(" continuation.finish(throwing: failure)");
|
|
1504
|
+
writer.line(" return");
|
|
1505
|
+
writer.line("}");
|
|
1506
|
+
writer.block("do", () => {
|
|
1507
|
+
writer.line("guard let reopened = try await reconnect.reopen(lastEventId) else {");
|
|
1508
|
+
writer.line(" continuation.finish()");
|
|
1509
|
+
writer.line(" return");
|
|
1471
1510
|
writer.line("}");
|
|
1511
|
+
writer.line("bytes = reopened");
|
|
1512
|
+
});
|
|
1513
|
+
writer.block("catch let error as URLError", () => {
|
|
1514
|
+
writer.line("failure = error");
|
|
1515
|
+
writer.line("bytes = nil");
|
|
1516
|
+
});
|
|
1517
|
+
writer.block("catch", () => {
|
|
1518
|
+
writer.line("continuation.finish(throwing: error)");
|
|
1519
|
+
writer.line("return");
|
|
1472
1520
|
});
|
|
1473
|
-
writer.line("continuation.finish()");
|
|
1474
1521
|
});
|
|
1475
|
-
writer.line("catch { continuation.finish(throwing: error) }");
|
|
1476
1522
|
});
|
|
1477
1523
|
writer.line("continuation.onTermination = { _ in task.cancel() }");
|
|
1478
1524
|
});
|
|
1479
1525
|
});
|
|
1480
1526
|
writer.blank();
|
|
1481
|
-
writer.block("static func events<Event: Sendable>(_ bytes: URLSession.AsyncBytes, using decoder: JSONDecoder, _ decode: @escaping @Sendable (ServerSentEvent, JSONDecoder) throws -> Event?) -> AsyncThrowingStream<Event, Swift.Error>", () => {
|
|
1527
|
+
writer.block("static func events<Event: Sendable>(_ bytes: URLSession.AsyncBytes, using decoder: JSONDecoder, reconnect: Reconnection? = nil, _ decode: @escaping @Sendable (ServerSentEvent, JSONDecoder) throws -> Event?) -> AsyncThrowingStream<Event, Swift.Error>", () => {
|
|
1482
1528
|
closure(writer, "AsyncThrowingStream { continuation in", () => {
|
|
1483
1529
|
writer.block("let task = Task", () => {
|
|
1484
1530
|
writer.block("do", () => {
|
|
1485
|
-
writer.block("for try await event in serverSentEvents(bytes)", () => {
|
|
1531
|
+
writer.block("for try await event in serverSentEvents(bytes, reconnect: reconnect)", () => {
|
|
1486
1532
|
writer.line("if let decoded = try decode(event, decoder) { continuation.yield(decoded) }");
|
|
1487
1533
|
});
|
|
1488
1534
|
writer.line("continuation.finish()");
|
|
@@ -1865,6 +1911,35 @@ const emitErrorCases = (writer, method, context, receiver, prelude) => {
|
|
|
1865
1911
|
}
|
|
1866
1912
|
}
|
|
1867
1913
|
};
|
|
1914
|
+
const IDEMPOTENT_METHODS = new Set([
|
|
1915
|
+
"GET",
|
|
1916
|
+
"HEAD",
|
|
1917
|
+
"OPTIONS",
|
|
1918
|
+
"PUT",
|
|
1919
|
+
"DELETE"
|
|
1920
|
+
]);
|
|
1921
|
+
/**
|
|
1922
|
+
* Writes the closure that reopens an event stream and returns the argument handing it to `Kizuna.events`, or nothing
|
|
1923
|
+
* for a method a client may not resend.
|
|
1924
|
+
*/
|
|
1925
|
+
const emitReopen = (writer, method, receiver, failure, status) => {
|
|
1926
|
+
if (!IDEMPOTENT_METHODS.has(method.method)) return "";
|
|
1927
|
+
closure(writer, `let reopen: @Sendable (String) async throws -> URLSession.AsyncBytes? = { [request, session = ${receiver}session, retry = ${receiver}retry, requestMiddleware = ${receiver}requestMiddleware, responseMiddleware = ${receiver}responseMiddleware] lastEventId in`, () => {
|
|
1928
|
+
writer.line("var again = request");
|
|
1929
|
+
writer.line("if !lastEventId.isEmpty { again.setValue(lastEventId, forHTTPHeaderField: \"Last-Event-ID\") }");
|
|
1930
|
+
writer.line("let reopened: (URLSession.AsyncBytes, Int, HTTPURLResponse)");
|
|
1931
|
+
writer.line(`do { reopened = try await Kizuna.open(&again, session: session, retry: retry, requestMiddleware: requestMiddleware, responseMiddleware: responseMiddleware, failure: ${failure}.self) }`);
|
|
1932
|
+
writer.line(`catch let failure as ${failure} {`);
|
|
1933
|
+
writer.line(" if case .requestFailed(let underlying) = failure { throw underlying }");
|
|
1934
|
+
writer.line(" throw failure");
|
|
1935
|
+
writer.line("}");
|
|
1936
|
+
writer.line("if reopened.1 == 204 { return nil }");
|
|
1937
|
+
writer.line(`guard reopened.1 == ${status} else { throw ${failure}.unexpectedStatus(reopened.1, Foundation.Data()) }`);
|
|
1938
|
+
writer.line(`guard reopened.2.value(forHTTPHeaderField: "Content-Type")?.lowercased().hasPrefix("text/event-stream") == true else { throw ${failure}.invalidResponse }`);
|
|
1939
|
+
writer.line("return reopened.0");
|
|
1940
|
+
});
|
|
1941
|
+
return `, reconnect: ${receiver}reconnect.map { Kizuna.Reconnection(policy: $0, reopen: reopen) }`;
|
|
1942
|
+
};
|
|
1868
1943
|
const emitStreamMethodTail = (writer, method, context, receiver, responseBinding) => {
|
|
1869
1944
|
const failure = failureRef(method, context);
|
|
1870
1945
|
const stream = method.stream;
|
|
@@ -1880,7 +1955,8 @@ const emitStreamMethodTail = (writer, method, context, receiver, responseBinding
|
|
|
1880
1955
|
else if (stream.mode === "binary") writer.line("let body = Kizuna.chunks(bytes)");
|
|
1881
1956
|
else if (stream.events) {
|
|
1882
1957
|
const events = stream.events;
|
|
1883
|
-
|
|
1958
|
+
const elementType = streamElementType(method, context, "actor");
|
|
1959
|
+
closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder${emitReopen(writer, method, receiver, failure, stream.status)}) { event, decoder -> ${elementType}? in`, () => {
|
|
1884
1960
|
writer.line("switch event.event {");
|
|
1885
1961
|
for (const event of events) {
|
|
1886
1962
|
const payloadType = resolveType(event.type, method.operationName, context);
|
|
@@ -1891,7 +1967,7 @@ const emitStreamMethodTail = (writer, method, context, receiver, responseBinding
|
|
|
1891
1967
|
});
|
|
1892
1968
|
} else {
|
|
1893
1969
|
const elementType = streamElementType(method, context, "actor");
|
|
1894
|
-
closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder) { event, decoder -> ${elementType}? in`, () => {
|
|
1970
|
+
closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder${emitReopen(writer, method, receiver, failure, stream.status)}) { event, decoder -> ${elementType}? in`, () => {
|
|
1895
1971
|
writer.line(`try decoder.decode(${elementType}.self, from: Foundation.Data(event.data.utf8))`);
|
|
1896
1972
|
});
|
|
1897
1973
|
}
|
|
@@ -2022,6 +2098,22 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
|
|
|
2022
2098
|
writer.line("self.maximumDelay = maximumDelay");
|
|
2023
2099
|
});
|
|
2024
2100
|
});
|
|
2101
|
+
writer.blank();
|
|
2102
|
+
writer.docComment("Reconnects an event stream on a GET, HEAD, OPTIONS, PUT or DELETE when its connection drops, sending the id of the last event as Last-Event-ID.");
|
|
2103
|
+
writer.block("public struct ReconnectPolicy: Sendable", () => {
|
|
2104
|
+
writer.docComment("Reconnections in a row before the stream fails. An event received resets the count.");
|
|
2105
|
+
writer.line("public var maximumAttempts: Int");
|
|
2106
|
+
writer.docComment("Seconds to wait before reconnecting, until the server sends a retry field.");
|
|
2107
|
+
writer.line("public var reconnectionTime: TimeInterval");
|
|
2108
|
+
writer.docComment("Seconds the wait may grow to after reconnections in a row that fail.");
|
|
2109
|
+
writer.line("public var maximumDelay: TimeInterval");
|
|
2110
|
+
writer.blank();
|
|
2111
|
+
writer.block("public init(maximumAttempts: Int = 5, reconnectionTime: TimeInterval = 3, maximumDelay: TimeInterval = 30)", () => {
|
|
2112
|
+
writer.line("self.maximumAttempts = maximumAttempts");
|
|
2113
|
+
writer.line("self.reconnectionTime = reconnectionTime");
|
|
2114
|
+
writer.line("self.maximumDelay = maximumDelay");
|
|
2115
|
+
});
|
|
2116
|
+
});
|
|
2025
2117
|
if (usesMultipart) {
|
|
2026
2118
|
writer.blank();
|
|
2027
2119
|
writer.block("public struct MultipartFile: Sendable, Equatable", () => {
|
|
@@ -2120,15 +2212,17 @@ const emitClient = (writer, config, partition, context, typesByOperation) => {
|
|
|
2120
2212
|
writer.line("public let requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)?");
|
|
2121
2213
|
writer.line("public let responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)?");
|
|
2122
2214
|
writer.line("public let retry: RetryPolicy?");
|
|
2215
|
+
writer.line("public let reconnect: ReconnectPolicy?");
|
|
2123
2216
|
writer.line("let encoder: JSONEncoder");
|
|
2124
2217
|
writer.line("let decoder: JSONDecoder");
|
|
2125
2218
|
writer.blank();
|
|
2126
2219
|
const contextInitParam = contextFields.length > 0 ? `, requestContext: RequestContext${contextRequired ? "" : " = RequestContext()"}` : "";
|
|
2127
|
-
writer.block(`public init(baseURL: URL, session: URLSession = .shared, timeout: TimeInterval = 30, retry: RetryPolicy? = nil${contextInitParam}, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)? = nil, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)? = nil)`, () => {
|
|
2220
|
+
writer.block(`public init(baseURL: URL, session: URLSession = .shared, timeout: TimeInterval = 30, retry: RetryPolicy? = nil, reconnect: ReconnectPolicy? = nil${contextInitParam}, requestMiddleware: (@Sendable (inout URLRequest) async throws -> Void)? = nil, responseMiddleware: (@Sendable (URLRequest, Foundation.Data, URLResponse) async -> Void)? = nil)`, () => {
|
|
2128
2221
|
writer.line("self.baseURL = baseURL");
|
|
2129
2222
|
writer.line("self.session = session");
|
|
2130
2223
|
writer.line("self.timeout = timeout");
|
|
2131
2224
|
writer.line("self.retry = retry");
|
|
2225
|
+
writer.line("self.reconnect = reconnect");
|
|
2132
2226
|
if (contextFields.length > 0) writer.line("self.requestContextHeaders = requestContext.headerFields");
|
|
2133
2227
|
writer.line("self.requestMiddleware = requestMiddleware");
|
|
2134
2228
|
writer.line("self.responseMiddleware = responseMiddleware");
|
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-D8fdPAdY.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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kizunajs/swift",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.21",
|
|
4
4
|
"description": "Swift client generation for Kizuna.js APIs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kizunajs",
|
|
@@ -59,14 +59,14 @@
|
|
|
59
59
|
"access": "public"
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@kizunajs/cli": "2.0.0-beta.
|
|
62
|
+
"@kizunajs/cli": "2.0.0-beta.21"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"kizunajs": "2.0.0-beta.
|
|
65
|
+
"kizunajs": "2.0.0-beta.21",
|
|
66
66
|
"zod": "^4.0.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"kizunajs": "2.0.0-beta.
|
|
69
|
+
"kizunajs": "2.0.0-beta.21",
|
|
70
70
|
"@types/node": "^24",
|
|
71
71
|
"tsdown": "^0.21.0",
|
|
72
72
|
"typescript": "^6.0.3",
|