@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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_generator = require("./generator-Bk6JRrTJ.cjs");
2
+ const require_generator = require("./generator-D8fdPAdY.cjs");
3
3
  let node_fs = require("node:fs");
4
4
  let node_path = require("node:path");
5
5
  let node_util = require("node:util");
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as generateSwiftClient } from "./generator-2GeebFHv.mjs";
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(_ bytes: URLSession.AsyncBytes) -> AsyncThrowingStream<ServerSentEvent, Swift.Error>", () => {
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 retry: Int? = nil");
1443
- writer.block("do", () => {
1444
- writer.block("for try await line in lines(bytes)", () => {
1445
- writer.line("if line.isEmpty {");
1446
- writer.line(" if !dataLines.isEmpty {");
1447
- writer.line(" continuation.yield(ServerSentEvent(event: eventType.isEmpty ? nil : eventType, data: dataLines.joined(separator: \"\\n\"), id: lastEventId.isEmpty ? nil : lastEventId, retry: retry))");
1448
- writer.line(" dataLines = []");
1449
- writer.line(" retry = nil");
1450
- writer.line(" }");
1451
- writer.line(" eventType = \"\"");
1452
- writer.line(" continue");
1453
- writer.line("}");
1454
- writer.line("if line.hasPrefix(\":\") { continue }");
1455
- writer.line("let field: Substring");
1456
- writer.line("var value: Substring");
1457
- writer.line("if let separator = line.firstIndex(of: \":\") {");
1458
- writer.line(" field = line[..<separator]");
1459
- writer.line(" value = line[line.index(after: separator)...]");
1460
- writer.line(" if value.hasPrefix(\" \") { value = value.dropFirst() }");
1461
- writer.line("} else {");
1462
- writer.line(" field = Substring(line)");
1463
- writer.line(" value = \"\"");
1464
- writer.line("}");
1465
- writer.line("switch field {");
1466
- writer.line("case \"event\": eventType = String(value)");
1467
- writer.line("case \"data\": dataLines.append(String(value))");
1468
- writer.line("case \"id\": if !value.contains(\"\\0\") { lastEventId = String(value) }");
1469
- writer.line("case \"retry\": if !value.isEmpty, value.allSatisfy(\\.isNumber), let milliseconds = Int(value) { retry = milliseconds }");
1470
- writer.line("default: break");
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
- closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder) { event, decoder -> ${streamElementType(method, context, "actor")}? in`, () => {
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(_ bytes: URLSession.AsyncBytes) -> AsyncThrowingStream<ServerSentEvent, Swift.Error>", () => {
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 retry: Int? = nil");
1443
- writer.block("do", () => {
1444
- writer.block("for try await line in lines(bytes)", () => {
1445
- writer.line("if line.isEmpty {");
1446
- writer.line(" if !dataLines.isEmpty {");
1447
- writer.line(" continuation.yield(ServerSentEvent(event: eventType.isEmpty ? nil : eventType, data: dataLines.joined(separator: \"\\n\"), id: lastEventId.isEmpty ? nil : lastEventId, retry: retry))");
1448
- writer.line(" dataLines = []");
1449
- writer.line(" retry = nil");
1450
- writer.line(" }");
1451
- writer.line(" eventType = \"\"");
1452
- writer.line(" continue");
1453
- writer.line("}");
1454
- writer.line("if line.hasPrefix(\":\") { continue }");
1455
- writer.line("let field: Substring");
1456
- writer.line("var value: Substring");
1457
- writer.line("if let separator = line.firstIndex(of: \":\") {");
1458
- writer.line(" field = line[..<separator]");
1459
- writer.line(" value = line[line.index(after: separator)...]");
1460
- writer.line(" if value.hasPrefix(\" \") { value = value.dropFirst() }");
1461
- writer.line("} else {");
1462
- writer.line(" field = Substring(line)");
1463
- writer.line(" value = \"\"");
1464
- writer.line("}");
1465
- writer.line("switch field {");
1466
- writer.line("case \"event\": eventType = String(value)");
1467
- writer.line("case \"data\": dataLines.append(String(value))");
1468
- writer.line("case \"id\": if !value.contains(\"\\0\") { lastEventId = String(value) }");
1469
- writer.line("case \"retry\": if !value.isEmpty, value.allSatisfy(\\.isNumber), let milliseconds = Int(value) { retry = milliseconds }");
1470
- writer.line("default: break");
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
- closure(writer, `let body = Kizuna.events(bytes, using: ${receiver}decoder) { event, decoder -> ${streamElementType(method, context, "actor")}? in`, () => {
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-Bk6JRrTJ.cjs");
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
@@ -1,4 +1,4 @@
1
- import { n as swiftClientWalk, t as generateSwiftClient } from "./generator-2GeebFHv.mjs";
1
+ import { n as swiftClientWalk, t as generateSwiftClient } from "./generator-BQDx47ez.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/swift",
3
- "version": "2.0.0-beta.20",
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.20"
62
+ "@kizunajs/cli": "2.0.0-beta.21"
63
63
  },
64
64
  "peerDependencies": {
65
- "kizunajs": "2.0.0-beta.20",
65
+ "kizunajs": "2.0.0-beta.21",
66
66
  "zod": "^4.0.0"
67
67
  },
68
68
  "devDependencies": {
69
- "kizunajs": "2.0.0-beta.20",
69
+ "kizunajs": "2.0.0-beta.21",
70
70
  "@types/node": "^24",
71
71
  "tsdown": "^0.21.0",
72
72
  "typescript": "^6.0.3",