vxrn 2.13.10 → 2.14.1

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.
@@ -1189,10 +1189,11 @@ function nativeSourceContract(file, source) {
1189
1189
  ...module.methods.map((method) => method.subscription ? ` ${method.name}(listener: (${method.subscription.payload.map((arg, index) => `value${index}: ${arg.type}`).join(", ")}) => void): () => void` : ` ${method.name}(${method.parameters.map((param) => `${param.name}: ${param.type}`).join(", ")}): Promise<${method.result}>`),
1190
1190
  "}"
1191
1191
  ].join("\n")));
1192
- declarations.push(...views.map((view) => `export declare function ${view.name}(props: {${view.props.map((prop) => ` ${prop.name}${prop.optional ? "?" : ""}: ${prop.type}`).join(";")} }): ReactElement`));
1192
+ declarations.push(...views.map((view) => `export declare function ${view.name}(props: {${view.props.map((prop) => ` ${prop.name}${prop.optional ? "?" : ""}: ${prop.type};`).join("")} style?: StyleProp<ViewStyle> }): ReactElement`));
1193
1193
  if (defaultView) {
1194
1194
  declarations.unshift("export default function SwiftPackage(props: Record<string, unknown>): ReactElement");
1195
1195
  }
1196
+ if (views.length > 0) declarations.unshift("import type { StyleProp, ViewStyle } from 'react-native'");
1196
1197
  if (defaultView || views.length > 0) declarations.unshift("import type { ReactElement } from 'react'");
1197
1198
  const declaration = `${GENERATED_HEADER}${declarations.join("\n\n")}
1198
1199
  `;
@@ -1420,35 +1421,147 @@ function renderSwiftSourceGlue(packageId, contracts) {
1420
1421
  " }",
1421
1422
  " }"
1422
1423
  ];
1423
- const renderViews = () => [
1424
- " private static func missing(_ path: String) throws -> Any {",
1425
- " throw NSError(domain: \"OneNativeSource\", code: 2, userInfo: [NSLocalizedDescriptionKey: \"\\(path): required\"])",
1426
- " }",
1427
- " static func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {",
1428
- " guard let props = try JSONSerialization.jsonObject(with: Data(propsJson.utf8)) as? [String: Any] else {",
1429
- " throw NSError(domain: \"OneNativeSource\", code: 2, userInfo: [NSLocalizedDescriptionKey: \"props must be a JSON object\"])",
1430
- " }",
1431
- " switch name {",
1432
- ...views.flatMap((view) => [
1433
- ` case ${JSON.stringify(view.name)}:`,
1434
- ` return AnyView(${view.name}(`,
1435
- ...view.props.map((prop, index) => {
1436
- const comma = index === view.props.length - 1 ? "" : ",";
1437
- const key = JSON.stringify(prop.name);
1438
- if (prop.callback) {
1439
- const params = prop.callback.map((arg, n) => `a${n}: ${arg.nativeType}`).join(", ");
1440
- const lambda = `{ (${params}) -> Void in emit(${key}, args([${prop.callback.map((_, n) => `a${n}`).join(", ")}])) }`;
1441
- return ` ${prop.name}: ${prop.optional ? `(props[${key}] as? Bool) == true ? ${lambda} : nil` : lambda}${comma}`;
1442
- }
1443
- const input = prop.optional ? `props[${key}] ?? NSNull()` : `props[${key}] ?? missing(${JSON.stringify(`props.${prop.name}`)})`;
1444
- return ` ${prop.name}: try decode(${input}, as: ${prop.nativeType}.self)${comma}`;
1424
+ const renderViews = () => {
1425
+ const notLinked2 = " default: throw NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"swift view \\(name) is not linked; rebuild the app\"])";
1426
+ const fieldsOf = (view) => view.props.filter((prop) => !prop.callback || prop.optional).map((prop, index) => {
1427
+ const type = prop.callback ? "Bool?" : prop.nativeType;
1428
+ const optional = type.endsWith("?");
1429
+ const list = /^\[(.*)\]\??$/.exec(type);
1430
+ return {
1431
+ name: prop.name,
1432
+ type,
1433
+ optional,
1434
+ list: list && !list[1].includes(":") ? list[1] : null,
1435
+ key: `p${index}`
1436
+ };
1437
+ });
1438
+ return [
1439
+ " private struct OneListPatch<E: Decodable>: Decodable {",
1440
+ " let length: Int",
1441
+ " let set: [(Int, E)]",
1442
+ " private struct Entry: Decodable {",
1443
+ " let index: Int",
1444
+ " let value: E",
1445
+ " init(from decoder: Decoder) throws {",
1446
+ " var c = try decoder.unkeyedContainer()",
1447
+ " index = try c.decode(Int.self)",
1448
+ " value = try c.decode(E.self)",
1449
+ " }",
1450
+ " }",
1451
+ " private enum CodingKeys: String, CodingKey { case length, set }",
1452
+ " init(from decoder: Decoder) throws {",
1453
+ " let c = try decoder.container(keyedBy: CodingKeys.self)",
1454
+ " length = try c.decode(Int.self, forKey: .length)",
1455
+ " set = try c.decode([Entry].self, forKey: .set).map { ($0.index, $0.value) }",
1456
+ " }",
1457
+ " }",
1458
+ " private nonisolated static func patch<E>(_ list: inout [E], _ patch: OneListPatch<E>) -> Bool {",
1459
+ " if patch.length < 0 { return false }",
1460
+ " if list.count > patch.length { list.removeLast(list.count - patch.length) }",
1461
+ " for (index, value) in patch.set {",
1462
+ " if index < list.count { list[index] = value } else if index == list.count && index < patch.length { list.append(value) } else { return false }",
1463
+ " }",
1464
+ " return list.count == patch.length",
1465
+ " }",
1466
+ " private nonisolated static func missing(_ prop: String) -> Error {",
1467
+ " NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"missing prop \\(prop)\"])",
1468
+ " }",
1469
+ ...views.flatMap((view) => {
1470
+ const fields = fieldsOf(view);
1471
+ const lists = fields.filter((field) => field.list);
1472
+ return [
1473
+ ...lists.length > 0 ? [
1474
+ ` private struct Lists_${view.name}: Decodable {`,
1475
+ ...lists.map((field) => ` let ${field.key}: OneListPatch<${field.list}>?`),
1476
+ ` private enum CodingKeys: String, CodingKey { ${lists.map((field) => `case ${field.key} = ${JSON.stringify(field.name)}`).join("; ")} }`,
1477
+ " }"
1478
+ ] : [],
1479
+ ` private struct Doc_${view.name}: Decodable {`,
1480
+ " let seq: Int",
1481
+ " let base: Int?",
1482
+ ...lists.length > 0 ? [` let lists: Lists_${view.name}?`] : [],
1483
+ ...fields.map((field) => ` let ${field.key}: Optional<${field.type}>`),
1484
+ ` private enum CodingKeys: String, CodingKey { ${[
1485
+ "case seq = \"$seq\"",
1486
+ "case base = \"$base\"",
1487
+ "case lists = \"$lists\"",
1488
+ ...fields.map((field) => `case ${field.key} = ${JSON.stringify(field.name)}`)
1489
+ ].join("; ")} }`,
1490
+ " init(from decoder: Decoder) throws {",
1491
+ " let c = try decoder.container(keyedBy: CodingKeys.self)",
1492
+ " seq = try c.decodeIfPresent(Int.self, forKey: .seq) ?? 0",
1493
+ " base = try c.decodeIfPresent(Int.self, forKey: .base)",
1494
+ ...lists.length > 0 ? [` lists = try c.decodeIfPresent(Lists_${view.name}.self, forKey: .lists)`] : [],
1495
+ ...fields.map((field) => ` ${field.key} = try c.contains(.${field.key}) ? Optional<${field.type}>.some(c.decode(${field.type}.self, forKey: .${field.key})) : Optional<${field.type}>.none`),
1496
+ " }",
1497
+ " }",
1498
+ ` private final class Held_${view.name} {`,
1499
+ " var oneSeq: Int",
1500
+ ...fields.map((field) => ` var \`${field.name}\`: ${field.type}`),
1501
+ ` init(_ d: Doc_${view.name}) throws {`,
1502
+ " oneSeq = d.seq",
1503
+ ...fields.flatMap((field) => field.optional ? [` \`${field.name}\` = d.${field.key}.flatMap { $0 }`] : [` guard let ${field.key} = d.${field.key} else { throw ${bodyName}.missing(${JSON.stringify(field.name)}) }`, ` \`${field.name}\` = ${field.key}`]),
1504
+ " }",
1505
+ ` func apply(_ d: Doc_${view.name}) -> Bool {`,
1506
+ " if d.base != oneSeq { return false }",
1507
+ ...lists.flatMap((field) => [
1508
+ ` var ${field.key} = \`${field.name}\``,
1509
+ ` if let v = d.${field.key} { ${field.key} = v }`,
1510
+ ` if let p = d.lists?.${field.key} {`,
1511
+ ...field.optional ? [
1512
+ ` guard var items = ${field.key} else { return false }`,
1513
+ ` if !${bodyName}.patch(&items, p) { return false }`,
1514
+ ` ${field.key} = items`
1515
+ ] : [` if !${bodyName}.patch(&${field.key}, p) { return false }`],
1516
+ " }"
1517
+ ]),
1518
+ ...fields.map((field) => field.list ? ` \`${field.name}\` = ${field.key}` : ` if let v = d.${field.key} { \`${field.name}\` = v }`),
1519
+ " oneSeq = d.seq",
1520
+ " return true",
1521
+ " }",
1522
+ " }"
1523
+ ];
1445
1524
  }),
1446
- " ))"
1447
- ]),
1448
- " default: throw NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"swift view \\(name) is not linked; rebuild the app\"])",
1449
- " }",
1450
- " }"
1451
- ];
1525
+ " static func update(_ held: AnyObject?, _ name: String, _ text: String) throws -> AnyObject? {",
1526
+ " let data = Data(text.utf8)",
1527
+ " switch name {",
1528
+ ...views.flatMap((view) => [
1529
+ ` case ${JSON.stringify(view.name)}:`,
1530
+ ` let doc = try decode(Doc_${view.name}.self, from: data)`,
1531
+ ` if doc.base == nil { return try Held_${view.name}(doc) }`,
1532
+ ` guard let h = held as? Held_${view.name}, h.apply(doc) else { return nil }`,
1533
+ " return h"
1534
+ ]),
1535
+ notLinked2,
1536
+ " }",
1537
+ " }",
1538
+ " static func build(_ held: AnyObject, _ name: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {",
1539
+ " switch name {",
1540
+ ...views.flatMap((view) => [
1541
+ ` case ${JSON.stringify(view.name)}:`,
1542
+ ` guard ${fieldsOf(view).length > 0 ? "let props = held as?" : "held is"} Held_${view.name} else { throw NSError(domain: "OneNativeSource", code: 1, userInfo: [NSLocalizedDescriptionKey: "props of another view for \\(name)"]) }`,
1543
+ ` return AnyView(${view.name}(`,
1544
+ ...view.props.map((prop, index) => {
1545
+ const comma = index === view.props.length - 1 ? "" : ",";
1546
+ const key = JSON.stringify(prop.name);
1547
+ if (prop.callback) {
1548
+ const params = prop.callback.map((arg, n) => `a${n}: ${arg.nativeType}`).join(", ");
1549
+ const lambda = `{ (${params}) -> Void in emit(${key}, args([${prop.callback.map((_, n) => `a${n}`).join(", ")}])) }`;
1550
+ return ` ${prop.name}: ${prop.optional ? `props.\`${prop.name}\` == true ? ${lambda} : nil` : lambda}${comma}`;
1551
+ }
1552
+ return ` ${prop.name}: props.\`${prop.name}\`${comma}`;
1553
+ }),
1554
+ " ))"
1555
+ ]),
1556
+ notLinked2,
1557
+ " }",
1558
+ " }",
1559
+ " static func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {",
1560
+ " guard let held = try update(nil, name, propsJson) else { throw NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"\\(name) needs a full props document\"]) }",
1561
+ " return try build(held, name, emit)",
1562
+ " }"
1563
+ ];
1564
+ };
1452
1565
  const source = [
1453
1566
  "import Foundation",
1454
1567
  "import SwiftUI",
@@ -1514,13 +1627,15 @@ function renderSwiftSourceGlue(packageId, contracts) {
1514
1627
  " if let date = (try? isoDateFractional.parse(text)) ?? (try? isoDate.parse(text)) { return date }",
1515
1628
  " throw DecodingError.dataCorruptedError(in: container, debugDescription: \"expected ISO date\")",
1516
1629
  " }",
1517
- " private static func decode<T: Decodable>(_ value: Any, as type: T.Type) throws -> T {",
1518
- " let data = try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed])",
1630
+ " private static func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {",
1519
1631
  " let decoder = JSONDecoder()",
1520
1632
  " decoder.dateDecodingStrategy = .custom { try decodeDate($0) }",
1521
1633
  " decoder.dataDecodingStrategy = .base64",
1522
1634
  " return try decoder.decode(T.self, from: data)",
1523
1635
  " }",
1636
+ " private static func decode<T: Decodable>(_ value: Any, as type: T.Type) throws -> T {",
1637
+ " try decode(T.self, from: try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed]))",
1638
+ " }",
1524
1639
  " private static func encode(_ value: any Encodable) throws -> String {",
1525
1640
  " let encoder = JSONEncoder()",
1526
1641
  " encoder.dateEncodingStrategy = .iso8601",
@@ -1588,6 +1703,8 @@ function renderSwiftSourceGlue(packageId, contracts) {
1588
1703
  " public required override init() {}",
1589
1704
  ` public var contractHash: String { ${bodyName}.hash }`,
1590
1705
  ` public func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView { try ${bodyName}.view(name, propsJson, emit) }`,
1706
+ ` public func updateView(_ held: AnyObject?, _ name: String, _ document: String) throws -> AnyObject? { try ${bodyName}.update(held, name, document) }`,
1707
+ ` public func buildView(_ held: AnyObject, _ name: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView { try ${bodyName}.build(held, name, emit) }`,
1591
1708
  "}"
1592
1709
  ] : [],
1593
1710
  "#endif",
@@ -1598,6 +1715,14 @@ function renderSwiftSourceGlue(packageId, contracts) {
1598
1715
  source
1599
1716
  };
1600
1717
  }
1718
+ function unboxedZero(type) {
1719
+ if (type === "Int") return "0";
1720
+ if (type === "Long") return "0L";
1721
+ if (type === "Double") return "0.0";
1722
+ if (type === "Float") return "0f";
1723
+ if (type === "Boolean") return "false";
1724
+ return null;
1725
+ }
1601
1726
  function renderKotlinSourceGlue(sourceId, contract) {
1602
1727
  if (contract.language !== "kotlin") throw new Error("kotlin glue received a swift contract");
1603
1728
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(sourceId)) throw new Error(`invalid kotlin source id ${sourceId}`);
@@ -1605,10 +1730,29 @@ function renderKotlinSourceGlue(sourceId, contract) {
1605
1730
  sourceId,
1606
1731
  contract: contract.hash
1607
1732
  })).digest("hex");
1733
+ const readerClass = `OneJsonReader_${sourceId}`;
1734
+ const tokenClass = `OneJsonToken_${sourceId}`;
1608
1735
  const structsByName = new Map(contract.structs.map((entry) => [entry.name, entry]));
1609
1736
  const enumsByName = new Map(contract.enums.map((entry) => [entry.name, entry]));
1610
1737
  const qualified = (name) => contract.packageName ? `${contract.packageName}.${name}` : name;
1611
1738
  const isDateType = (type) => type === "Date" || type === "java.util.Date";
1739
+ const qualifiedType = (type) => {
1740
+ if (type.endsWith("?")) return `${qualifiedType(type.slice(0, -1))}?`;
1741
+ if (type.startsWith("List<") && type.endsWith(">")) return `List<${qualifiedType(type.slice(5, -1))}>`;
1742
+ if (type.startsWith("Map<String,") && type.endsWith(">")) return `Map<String, ${qualifiedType(type.slice(11, -1))}>`;
1743
+ if (structsByName.has(type) || enumsByName.has(type)) return qualified(type);
1744
+ if (isDateType(type)) return "java.util.Date";
1745
+ if ([
1746
+ "String",
1747
+ "Boolean",
1748
+ "Double",
1749
+ "Float",
1750
+ "Int",
1751
+ "Long",
1752
+ "ByteArray"
1753
+ ].includes(type)) return type;
1754
+ throw new Error(`unsupported kotlin glue type ${type}`);
1755
+ };
1612
1756
  const mentions = (type, match) => {
1613
1757
  let rest = type;
1614
1758
  while (rest.endsWith("?")) rest = rest.slice(0, -1);
@@ -1628,25 +1772,25 @@ function renderKotlinSourceGlue(sourceId, contract) {
1628
1772
  const needsEvents = contract.modules.some((module) => module.methods.some((method) => method.subscription));
1629
1773
  const needsDate = nativeTypes.some((type) => mentions(type, isDateType));
1630
1774
  const needsData = nativeTypes.some((type) => mentions(type, (leaf) => leaf === "ByteArray"));
1631
- const value = (type, input, path) => {
1775
+ const value = (type, reader, path) => {
1632
1776
  if (type.endsWith("?")) {
1633
- return `(${input}).let { raw -> if (raw == null || raw == JSONObject.NULL) null else ${value(type.slice(0, -1), "raw", path)} }`;
1634
- }
1635
- if (type === "String") return `readString(${input}, ${path})`;
1636
- if (type === "Boolean") return `readBoolean(${input}, ${path})`;
1637
- if (type === "Double") return `readDouble(${input}, ${path})`;
1638
- if (type === "Float") return `readDouble(${input}, ${path}).toFloat()`;
1639
- if (type === "Int") return `readInt(${input}, ${path})`;
1640
- if (type === "Long") return `readLong(${input}, ${path})`;
1641
- if (isDateType(type)) return `readDate(${input}, ${path})`;
1642
- if (type === "ByteArray") return `readData(${input}, ${path})`;
1643
- if (structsByName.has(type)) return `read${type}(${input}, ${path})`;
1644
- if (enumsByName.has(type)) return `read${type}(${input}, ${path})`;
1777
+ return `(if (${reader}.peek() == ${tokenClass}.NULL) { ${reader}.nextNull(); null } else ${value(type.slice(0, -1), reader, path)})`;
1778
+ }
1779
+ if (type === "String") return `readString(${reader}, ${path})`;
1780
+ if (type === "Boolean") return `readBoolean(${reader}, ${path})`;
1781
+ if (type === "Double") return `readDouble(${reader}, ${path})`;
1782
+ if (type === "Float") return `readDouble(${reader}, ${path}).toFloat()`;
1783
+ if (type === "Int") return `readInt(${reader}, ${path})`;
1784
+ if (type === "Long") return `readLong(${reader}, ${path})`;
1785
+ if (isDateType(type)) return `readDate(${reader}, ${path})`;
1786
+ if (type === "ByteArray") return `readData(${reader}, ${path})`;
1787
+ if (structsByName.has(type)) return `read${type}(${reader}, ${path})`;
1788
+ if (enumsByName.has(type)) return `read${type}(${reader}, ${path})`;
1645
1789
  if (type.startsWith("List<") && type.endsWith(">")) {
1646
- return `readList(${input}, ${path}) { item, itemPath -> ${value(type.slice(5, -1), "item", "itemPath")} }`;
1790
+ return `readList(${reader}, ${path}) { itemReader, itemPath -> ${value(type.slice(5, -1), "itemReader", "itemPath")} }`;
1647
1791
  }
1648
1792
  if (type.startsWith("Map<String,") && type.endsWith(">")) {
1649
- return `readMap(${input}, ${path}) { item, itemPath -> ${value(type.slice(11, -1), "item", "itemPath")} }`;
1793
+ return `readMap(${reader}, ${path}) { itemReader, itemPath -> ${value(type.slice(11, -1), "itemReader", "itemPath")} }`;
1650
1794
  }
1651
1795
  throw new Error(`unsupported kotlin glue type ${type}`);
1652
1796
  };
@@ -1666,15 +1810,38 @@ function renderKotlinSourceGlue(sourceId, contract) {
1666
1810
  }
1667
1811
  return `JSONObject.wrap(${accessor})`;
1668
1812
  };
1813
+ const cap = (name) => name.slice(0, 1).toUpperCase() + name.slice(1);
1669
1814
  const structHelpers = contract.structs.flatMap((struct) => [
1670
- `private fun read${struct.name}(value: Any, path: String): ${qualified(struct.name)} {`,
1671
- " val obj = value as? JSONObject ?: error(\"$path: expected object\")",
1815
+ `private fun read${struct.name}(reader: ${readerClass}, path: String): ${qualified(struct.name)} {`,
1816
+ ` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("$path: expected object")`,
1817
+ " reader.beginObject()",
1818
+ ...struct.fields.flatMap((field) => {
1819
+ const declared = qualifiedType(field.nativeType);
1820
+ if (field.optional) return [` var ${field.name}: ${declared} = ${field.fallback}`];
1821
+ const zero = unboxedZero(field.nativeType);
1822
+ if (zero !== null) return [` var ${field.name} = ${zero}`, ` var has${cap(field.name)} = false`];
1823
+ const boxed = declared.endsWith("?") ? declared : `${declared}?`;
1824
+ return [` var ${field.name}: ${boxed} = null`, ...field.nativeType.endsWith("?") ? [` var has${cap(field.name)} = false`] : []];
1825
+ }),
1826
+ " while (reader.hasNext()) {",
1827
+ ` when (reader.selectName(${struct.fields.map((field) => JSON.stringify(field.name)).join(", ")})) {`,
1828
+ ...struct.fields.map((field, index) => {
1829
+ const fieldPath = `"$path.${field.name}"`;
1830
+ const read = `${field.name} = ${value(field.nativeType, "reader", fieldPath)}`;
1831
+ const needsFlag = !field.optional && (field.nativeType.endsWith("?") || unboxedZero(field.nativeType) !== null);
1832
+ return needsFlag ? ` ${index} -> { ${read}; has${cap(field.name)} = true }` : ` ${index} -> ${read}`;
1833
+ }),
1834
+ " else -> reader.skipValue()",
1835
+ " }",
1836
+ " }",
1837
+ " reader.endObject()",
1838
+ ...struct.fields.flatMap((field) => !field.optional && unboxedZero(field.nativeType) !== null ? [` if (!has${cap(field.name)}) error("$path.${field.name}: required")`] : []),
1672
1839
  ` return ${qualified(struct.name)}(`,
1673
1840
  ...struct.fields.map((field) => {
1674
- const fieldPath = `"$path.${field.name}"`;
1675
- const required = `error("$path.${field.name}: required")`;
1676
- const input = field.optional ? `(if (obj.has(${JSON.stringify(field.name)})) obj.get(${JSON.stringify(field.name)}) else ${field.fallback ?? required})` : `(obj.opt(${JSON.stringify(field.name)}) ?: ${required})`;
1677
- return ` ${field.name} = ${value(field.nativeType, input, fieldPath)},`;
1841
+ if (field.optional) return ` ${field.name} = ${field.name},`;
1842
+ if (field.nativeType.endsWith("?")) return ` ${field.name} = (if (has${cap(field.name)}) ${field.name} else error("$path.${field.name}: required")),`;
1843
+ if (unboxedZero(field.nativeType) !== null) return ` ${field.name} = ${field.name},`;
1844
+ return ` ${field.name} = (${field.name} ?: error("$path.${field.name}: required")),`;
1678
1845
  }),
1679
1846
  " )",
1680
1847
  "}",
@@ -1684,47 +1851,329 @@ function renderKotlinSourceGlue(sourceId, contract) {
1684
1851
  "}"
1685
1852
  ]);
1686
1853
  const enumHelpers = contract.enums.flatMap((entry) => [
1687
- `private fun read${entry.name}(value: Any, path: String): ${qualified(entry.name)} {`,
1688
- " val text = readString(value, path)",
1854
+ `private fun read${entry.name}(reader: ${readerClass}, path: String): ${qualified(entry.name)} {`,
1855
+ " val text = readString(reader, path)",
1689
1856
  ` return try { ${qualified(entry.name)}.valueOf(text) } catch (e: IllegalArgumentException) { error("$path: expected one of ${entry.cases.map((item) => item.name).join(", ")}") }`,
1690
1857
  "}"
1691
1858
  ]);
1692
1859
  const source = [
1693
1860
  "import androidx.annotation.Keep",
1694
- ...contract.views.length === 0 ? [] : ["import androidx.compose.runtime.Composable", "import dev.onejs.one.OneNativeSourceViewDispatch"],
1861
+ ...contract.views.length === 0 ? [] : [
1862
+ "import androidx.compose.runtime.Composable",
1863
+ "import dev.onejs.one.OneNativeSourceKeyedState",
1864
+ "import dev.onejs.one.OneNativeSourceViewDispatch"
1865
+ ],
1695
1866
  "import dev.onejs.one.OneNativeSourceDispatch",
1696
1867
  ...needsEvents ? ["import dev.onejs.one.source.OneNativeSourceEvents"] : [],
1697
1868
  "import org.json.JSONArray",
1698
1869
  "import org.json.JSONObject",
1699
1870
  "",
1700
- "private fun readString(value: Any, path: String): String = value as? String ?: error(\"$path: expected string\")",
1701
- "private fun readBoolean(value: Any, path: String): Boolean = value as? Boolean ?: error(\"$path: expected boolean\")",
1702
- "private fun readDouble(value: Any, path: String): Double {",
1703
- " val result = (value as? Number)?.toDouble() ?: error(\"$path: expected number\")",
1871
+ `private enum class ${tokenClass} { BEGIN_OBJECT, END_OBJECT, BEGIN_ARRAY, END_ARRAY, STRING, NUMBER, BOOLEAN, NULL }`,
1872
+ "",
1873
+ "// a forward-only json reader over the props text: values decode straight",
1874
+ "// into typed props in one pass with no intermediate JSONObject. pure",
1875
+ "// kotlin with no java or android imports, so the same glue compiles for",
1876
+ "// kotlin/wasm.",
1877
+ `private class ${readerClass}(private val text: String) {`,
1878
+ " private var pos = 0",
1879
+ "",
1880
+ " private fun skipSpace() {",
1881
+ " while (pos < text.length) {",
1882
+ " when (text[pos]) {",
1883
+ " ' ', '\\t', '\\n', '\\r' -> pos++",
1884
+ " else -> return",
1885
+ " }",
1886
+ " }",
1887
+ " }",
1888
+ "",
1889
+ " private fun fail(message: String): Nothing = throw IllegalStateException(\"$message near offset $pos\")",
1890
+ "",
1891
+ ` fun peek(): ${tokenClass} {`,
1892
+ " skipSpace()",
1893
+ " if (pos >= text.length) fail(\"unexpected end of json\")",
1894
+ " return when (text[pos]) {",
1895
+ ` '{' -> ${tokenClass}.BEGIN_OBJECT`,
1896
+ ` '}' -> ${tokenClass}.END_OBJECT`,
1897
+ ` '[' -> ${tokenClass}.BEGIN_ARRAY`,
1898
+ ` ']' -> ${tokenClass}.END_ARRAY`,
1899
+ ` '"' -> ${tokenClass}.STRING`,
1900
+ ` 't', 'f' -> ${tokenClass}.BOOLEAN`,
1901
+ ` 'n' -> ${tokenClass}.NULL`,
1902
+ ` '-', in '0'..'9' -> ${tokenClass}.NUMBER`,
1903
+ " else -> fail(\"unexpected character\")",
1904
+ " }",
1905
+ " }",
1906
+ "",
1907
+ " private fun expect(char: Char) {",
1908
+ " skipSpace()",
1909
+ ` if (pos >= text.length || text[pos] != char) fail("expected '$char'")`,
1910
+ " pos++",
1911
+ " }",
1912
+ "",
1913
+ " fun beginObject() = expect('{')",
1914
+ " fun endObject() = expect('}')",
1915
+ " fun beginArray() = expect('[')",
1916
+ " fun endArray() = expect(']')",
1917
+ "",
1918
+ " // inside an object or array: true when another value follows, consuming",
1919
+ " // a comma first; false at the closing bracket.",
1920
+ " fun hasNext(): Boolean {",
1921
+ " skipSpace()",
1922
+ " if (pos >= text.length) fail(\"unexpected end of json\")",
1923
+ " if (text[pos] == ',') {",
1924
+ " pos++",
1925
+ " return true",
1926
+ " }",
1927
+ " return text[pos] != '}' && text[pos] != ']'",
1928
+ " }",
1929
+ "",
1930
+ " fun nextName(): String {",
1931
+ " skipSpace()",
1932
+ ` if (pos >= text.length || text[pos] != '"') fail("expected a name")`,
1933
+ " val name = readString()",
1934
+ " skipSpace()",
1935
+ ` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
1936
+ " pos++",
1937
+ " return name",
1938
+ " }",
1939
+ "",
1940
+ " fun nextString(): String {",
1941
+ " skipSpace()",
1942
+ ` if (pos >= text.length || text[pos] != '"') fail("expected a string")`,
1943
+ " return readString()",
1944
+ " }",
1945
+ "",
1946
+ " // matches an object name against the expected keys without allocating",
1947
+ " // it: the index of the match, or -1 for an unknown key. the name and",
1948
+ " // colon are consumed either way, so the caller skips the value on -1.",
1949
+ " fun selectName(vararg options: String): Int {",
1950
+ " skipSpace()",
1951
+ ` if (pos >= text.length || text[pos] != '"') fail("expected a name")`,
1952
+ " var end = pos + 1",
1953
+ ` while (end < text.length && text[end] != '"' && text[end] != '\\\\') end++`,
1954
+ " if (end < text.length && text[end] == '\\\\') {",
1955
+ " val decoded = readString()",
1956
+ " skipSpace()",
1957
+ ` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
1958
+ " pos++",
1959
+ " return options.indexOf(decoded)",
1960
+ " }",
1961
+ " if (end >= text.length) fail(\"unterminated string\")",
1962
+ " val length = end - pos - 1",
1963
+ " var index = -1",
1964
+ " for (i in options.indices) {",
1965
+ " if (options[i].length == length && text.regionMatches(pos + 1, options[i], 0, length)) { index = i; break }",
1966
+ " }",
1967
+ " pos = end + 1",
1968
+ " skipSpace()",
1969
+ ` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
1970
+ " pos++",
1971
+ " return index",
1972
+ " }",
1973
+ "",
1974
+ " private fun readString(): String {",
1975
+ " pos++ // the opening quote",
1976
+ " var end = pos",
1977
+ ` while (end < text.length && text[end] != '"' && text[end] != '\\\\') end++`,
1978
+ ` if (end < text.length && text[end] == '"') {`,
1979
+ " val out = text.substring(pos, end)",
1980
+ " pos = end + 1",
1981
+ " return out",
1982
+ " }",
1983
+ " val out = StringBuilder(text.substring(pos, end))",
1984
+ " pos = end",
1985
+ " while (true) {",
1986
+ " if (pos >= text.length) fail(\"unterminated string\")",
1987
+ " val char = text[pos++]",
1988
+ ` if (char == '"') return out.toString()`,
1989
+ " if (char != '\\\\') {",
1990
+ " out.append(char)",
1991
+ " continue",
1992
+ " }",
1993
+ " if (pos >= text.length) fail(\"unterminated escape\")",
1994
+ " when (text[pos++]) {",
1995
+ ` '"', '\\\\', '/' -> out.append(text[pos - 1])`,
1996
+ " 'b' -> out.append('\\b')",
1997
+ " 'f' -> out.append('\\u000C')",
1998
+ " 'n' -> out.append('\\n')",
1999
+ " 'r' -> out.append('\\r')",
2000
+ " 't' -> out.append('\\t')",
2001
+ " 'u' -> {",
2002
+ " if (pos + 4 > text.length) fail(\"bad unicode escape\")",
2003
+ " var code = 0",
2004
+ " for (i in 0 until 4) {",
2005
+ " val digit = text[pos + i].lowercaseChar()",
2006
+ ` val value = if (digit in '0'..'9') digit - '0' else if (digit in 'a'..'f') digit - 'a' + 10 else fail("bad unicode escape")`,
2007
+ " code = code * 16 + value",
2008
+ " }",
2009
+ " pos += 4",
2010
+ " out.append(code.toChar())",
2011
+ " }",
2012
+ " else -> fail(\"bad escape\")",
2013
+ " }",
2014
+ " }",
2015
+ " }",
2016
+ "",
2017
+ " fun nextDouble(): Double {",
2018
+ " skipSpace()",
2019
+ " val start = pos",
2020
+ " while (pos < text.length && (text[pos] in '0'..'9' || text[pos] == '.' || text[pos] == 'e' || text[pos] == 'E' || text[pos] == '+' || text[pos] == '-')) pos++",
2021
+ " if (pos == start) fail(\"expected a number\")",
2022
+ " return text.substring(start, pos).toDoubleOrNull() ?: fail(\"bad number\")",
2023
+ " }",
2024
+ "",
2025
+ " // plain-digit fast paths: the value when the token at pos is an integer",
2026
+ " // in range, null when it needs the double slow path. nothing is",
2027
+ " // consumed on null, so the caller falls back to nextDouble.",
2028
+ " fun nextIntOrNull(): Int? {",
2029
+ " var i = pos",
2030
+ " val negative = i < text.length && text[i] == '-'",
2031
+ " if (negative) i++",
2032
+ " var value = 0L",
2033
+ " val digits = i",
2034
+ " while (i < text.length && text[i] in '0'..'9') {",
2035
+ " value = value * 10 + (text[i] - '0')",
2036
+ " if (value > 2147483648L) return null",
2037
+ " i++",
2038
+ " }",
2039
+ " if (i == digits) return null",
2040
+ " if (i < text.length && (text[i] == '.' || text[i] == 'e' || text[i] == 'E')) return null",
2041
+ " if (!negative && value > 2147483647L) return null",
2042
+ " pos = i",
2043
+ " return if (negative) -value.toInt() else value.toInt()",
2044
+ " }",
2045
+ "",
2046
+ " fun nextLongOrNull(): Long? {",
2047
+ " var i = pos",
2048
+ " val negative = i < text.length && text[i] == '-'",
2049
+ " if (negative) i++",
2050
+ " var value = 0L",
2051
+ " val digits = i",
2052
+ " while (i < text.length && text[i] in '0'..'9') {",
2053
+ " val digit = (text[i] - '0').toLong()",
2054
+ " if (value > (9007199254740991L - digit) / 10) return null",
2055
+ " value = value * 10 + digit",
2056
+ " i++",
2057
+ " }",
2058
+ " if (i == digits) return null",
2059
+ " if (i < text.length && (text[i] == '.' || text[i] == 'e' || text[i] == 'E')) return null",
2060
+ " pos = i",
2061
+ " return if (negative) -value else value",
2062
+ " }",
2063
+ "",
2064
+ " fun nextBoolean(): Boolean {",
2065
+ " skipSpace()",
2066
+ " if (text.startsWith(\"true\", pos)) {",
2067
+ " pos += 4",
2068
+ " return true",
2069
+ " }",
2070
+ " if (text.startsWith(\"false\", pos)) {",
2071
+ " pos += 5",
2072
+ " return false",
2073
+ " }",
2074
+ " fail(\"expected a boolean\")",
2075
+ " }",
2076
+ "",
2077
+ " fun nextNull() {",
2078
+ " skipSpace()",
2079
+ " if (!text.startsWith(\"null\", pos)) fail(\"expected null\")",
2080
+ " pos += 4",
2081
+ " }",
2082
+ "",
2083
+ " fun skipValue() {",
2084
+ " when (peek()) {",
2085
+ ` ${tokenClass}.BEGIN_OBJECT -> {`,
2086
+ " beginObject()",
2087
+ " while (hasNext()) {",
2088
+ " nextName()",
2089
+ " skipValue()",
2090
+ " }",
2091
+ " endObject()",
2092
+ " }",
2093
+ ` ${tokenClass}.BEGIN_ARRAY -> {`,
2094
+ " beginArray()",
2095
+ " while (hasNext()) {",
2096
+ " skipValue()",
2097
+ " }",
2098
+ " endArray()",
2099
+ " }",
2100
+ ` ${tokenClass}.STRING -> nextString()`,
2101
+ ` ${tokenClass}.NUMBER -> nextDouble()`,
2102
+ ` ${tokenClass}.BOOLEAN -> nextBoolean()`,
2103
+ ` ${tokenClass}.NULL -> nextNull()`,
2104
+ " else -> fail(\"unexpected value\")",
2105
+ " }",
2106
+ " }",
2107
+ "",
2108
+ " fun endDocument() {",
2109
+ " skipSpace()",
2110
+ " if (pos != text.length) fail(\"trailing characters\")",
2111
+ " }",
2112
+ "}",
2113
+ "",
2114
+ `private fun readString(reader: ${readerClass}, path: String): String {`,
2115
+ ` if (reader.peek() != ${tokenClass}.STRING) error("$path: expected string")`,
2116
+ " return reader.nextString()",
2117
+ "}",
2118
+ `private fun readBoolean(reader: ${readerClass}, path: String): Boolean {`,
2119
+ ` if (reader.peek() != ${tokenClass}.BOOLEAN) error("$path: expected boolean")`,
2120
+ " return reader.nextBoolean()",
2121
+ "}",
2122
+ `private fun readDouble(reader: ${readerClass}, path: String): Double {`,
2123
+ ` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
2124
+ " val result = reader.nextDouble()",
1704
2125
  " if (!result.isFinite()) error(\"$path: expected finite number\")",
1705
2126
  " return result",
1706
2127
  "}",
1707
- "private fun readInt(value: Any, path: String): Int {",
1708
- " val result = readDouble(value, path)",
2128
+ `private fun readInt(reader: ${readerClass}, path: String): Int {`,
2129
+ ` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
2130
+ " reader.nextIntOrNull()?.let { return it }",
2131
+ " val result = readDouble(reader, path)",
1709
2132
  " if (result % 1.0 != 0.0 || result < Int.MIN_VALUE || result > Int.MAX_VALUE) error(\"$path: expected Int\")",
1710
2133
  " return result.toInt()",
1711
2134
  "}",
1712
- "private fun readLong(value: Any, path: String): Long {",
1713
- " val result = readDouble(value, path)",
2135
+ `private fun readLong(reader: ${readerClass}, path: String): Long {`,
2136
+ ` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
2137
+ " reader.nextLongOrNull()?.let { return it }",
2138
+ " val result = readDouble(reader, path)",
1714
2139
  " if (result % 1.0 != 0.0 || result < -9007199254740991.0 || result > 9007199254740991.0) error(\"$path: expected safe Long\")",
1715
2140
  " return result.toLong()",
1716
2141
  "}",
1717
- "private fun <T> readList(value: Any, path: String, read: (Any, String) -> T): List<T> {",
1718
- " val array = value as? JSONArray ?: error(\"$path: expected array\")",
1719
- " return (0 until array.length()).map { index -> read(array.get(index), \"$path[$index]\") }",
2142
+ "// the host sends true for each callback the element passed; any other",
2143
+ "// value reads as absent.",
2144
+ `private fun readOptBoolean(reader: ${readerClass}): Boolean {`,
2145
+ ` if (reader.peek() != ${tokenClass}.BOOLEAN) {`,
2146
+ " reader.skipValue()",
2147
+ " return false",
2148
+ " }",
2149
+ " return reader.nextBoolean()",
1720
2150
  "}",
1721
- "private fun <T> readMap(value: Any, path: String, read: (Any, String) -> T): Map<String, T> {",
1722
- " val objectValue = value as? JSONObject ?: error(\"$path: expected object\")",
1723
- " return objectValue.keys().asSequence().associateWith { key -> read(objectValue.get(key), \"$path.$key\") }",
2151
+ `private fun <T> readList(reader: ${readerClass}, path: String, read: (${readerClass}, String) -> T): List<T> {`,
2152
+ ` if (reader.peek() != ${tokenClass}.BEGIN_ARRAY) error("$path: expected array")`,
2153
+ " reader.beginArray()",
2154
+ " val result = mutableListOf<T>()",
2155
+ " var index = 0",
2156
+ " while (reader.hasNext()) {",
2157
+ " result.add(read(reader, \"$path[$index]\"))",
2158
+ " index++",
2159
+ " }",
2160
+ " reader.endArray()",
2161
+ " return result",
2162
+ "}",
2163
+ `private fun <T> readMap(reader: ${readerClass}, path: String, read: (${readerClass}, String) -> T): Map<String, T> {`,
2164
+ ` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("$path: expected object")`,
2165
+ " reader.beginObject()",
2166
+ " val result = mutableMapOf<String, T>()",
2167
+ " while (reader.hasNext()) {",
2168
+ " val key = reader.nextName()",
2169
+ " result[key] = read(reader, \"$path.$key\")",
2170
+ " }",
2171
+ " reader.endObject()",
2172
+ " return result",
1724
2173
  "}",
1725
2174
  ...needsDate ? [
1726
- "private fun readDate(value: Any, path: String): java.util.Date {",
1727
- " val text = readString(value, path)",
2175
+ `private fun readDate(reader: ${readerClass}, path: String): java.util.Date {`,
2176
+ " val text = readString(reader, path)",
1728
2177
  ` for (pattern in arrayOf("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "yyyy-MM-dd'T'HH:mm:ss'Z'")) {`,
1729
2178
  " try {",
1730
2179
  " return java.text.SimpleDateFormat(pattern, java.util.Locale.US).apply { timeZone = java.util.TimeZone.getTimeZone(\"UTC\"); isLenient = false }.parse(text) ?: error(\"$path: expected date\")",
@@ -1736,9 +2185,9 @@ function renderKotlinSourceGlue(sourceId, contract) {
1736
2185
  ` java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US).apply { timeZone = java.util.TimeZone.getTimeZone("UTC") }.format(value)`
1737
2186
  ] : [],
1738
2187
  ...needsData ? [
1739
- "private fun readData(value: Any, path: String): ByteArray {",
2188
+ `private fun readData(reader: ${readerClass}, path: String): ByteArray {`,
1740
2189
  " try {",
1741
- " return android.util.Base64.decode(readString(value, path), android.util.Base64.NO_WRAP)",
2190
+ " return android.util.Base64.decode(readString(reader, path), android.util.Base64.NO_WRAP)",
1742
2191
  " } catch (decodeError: IllegalArgumentException) {",
1743
2192
  " error(\"$path: expected base64 data\")",
1744
2193
  " }",
@@ -1750,7 +2199,9 @@ function renderKotlinSourceGlue(sourceId, contract) {
1750
2199
  `@Keep class OneNativeSource_${sourceId}: OneNativeSourceDispatch {`,
1751
2200
  ` override val contractHash = ${JSON.stringify(hash)}`,
1752
2201
  " override suspend fun call(module: String, method: String, argsJson: String): String {",
1753
- " val args = JSONArray(argsJson)",
2202
+ ` val reader = ${readerClass}(argsJson)`,
2203
+ ` if (reader.peek() != ${tokenClass}.BEGIN_ARRAY) error("args must be a JSON array")`,
2204
+ " reader.beginArray()",
1754
2205
  " return when (module) {",
1755
2206
  ...contract.modules.flatMap((module) => [
1756
2207
  ` ${JSON.stringify(`${sourceId}.${module.name}`)} -> when (method) {`,
@@ -1761,16 +2212,24 @@ function renderKotlinSourceGlue(sourceId, contract) {
1761
2212
  const closure = subscription.payload.length === 0 ? `{ OneNativeSourceEvents.send(${JSON.stringify(channel)}, JSONArray().toString()) }` : `{ ${subscription.payload.map((_, index) => `a${index}`).join(", ")} -> OneNativeSourceEvents.send(${JSON.stringify(channel)}, JSONArray()${subscription.payload.map((arg, index) => `.put(${encode(arg.nativeType, `a${index}`)})`).join("")}.toString()) }`;
1762
2213
  return [
1763
2214
  ` ${JSON.stringify(method.name)} -> {`,
1764
- ` require(args.length() == 0) { "${sourceId}.${module.name}.${method.name} subscribes; it takes no arguments" }`,
2215
+ ` require(!reader.hasNext()) { "${sourceId}.${module.name}.${method.name} subscribes; it takes no arguments" }`,
2216
+ " reader.endArray()",
2217
+ " reader.endDocument()",
1765
2218
  ` ${qualified(module.name)}.${method.name}(${closure})`,
1766
2219
  " \"null\"",
1767
2220
  " }"
1768
2221
  ];
1769
2222
  }
1770
- const lines = [` ${JSON.stringify(method.name)} -> {`, ` require(args.length() == ${method.parameters.length}) { "${sourceId}.${module.name}.${method.name} expects ${method.parameters.length} arguments" }`];
2223
+ const lines = [` ${JSON.stringify(method.name)} -> {`];
2224
+ const arityMessage = `${sourceId}.${module.name}.${method.name} expects ${method.parameters.length} arguments`;
1771
2225
  method.parameters.forEach((parameter, index) => {
1772
- lines.push(` val arg${index} = ${value(parameter.nativeType, `args.get(${index})`, JSON.stringify(`args[${index}]`))}`);
2226
+ lines.push(` val arg${index} = ${value(parameter.nativeType, "reader", JSON.stringify(`args[${index}]`))}`);
2227
+ if (index < method.parameters.length - 1) {
2228
+ lines.push(` require(reader.hasNext()) { ${JSON.stringify(arityMessage)} }`);
2229
+ }
1773
2230
  });
2231
+ lines.push(` require(!reader.hasNext()) { ${JSON.stringify(arityMessage)} }`);
2232
+ lines.push(" reader.endArray()", " reader.endDocument()");
1774
2233
  const call = `${qualified(module.name)}.${method.name}(${method.parameters.map((_, index) => `arg${index}`).join(", ")})`;
1775
2234
  if (method.result === "void") {
1776
2235
  lines.push(` ${call}`, " \"null\"");
@@ -1791,38 +2250,224 @@ function renderKotlinSourceGlue(sourceId, contract) {
1791
2250
  " }",
1792
2251
  "}",
1793
2252
  "",
1794
- ...contract.views.length === 0 ? [] : viewGlue(sourceId, contract, hash, value, encode)
2253
+ ...contract.views.length === 0 ? [] : viewGlue(sourceId, contract, hash, value, encode, qualifiedType)
1795
2254
  ].join("\n");
1796
2255
  return {
1797
2256
  hash,
1798
2257
  source
1799
2258
  };
1800
2259
  }
1801
- function viewGlue(sourceId, contract, hash, value, encode) {
2260
+ function viewGlue(sourceId, contract, hash, value, encode, qualifiedType) {
1802
2261
  const qualifier = contract.packageName ? `${contract.packageName}.` : "";
2262
+ const cap = (name) => name.slice(0, 1).toUpperCase() + name.slice(1);
2263
+ const readerClass = `OneJsonReader_${sourceId}`;
2264
+ const tokenClass = `OneJsonToken_${sourceId}`;
2265
+ const SEQ_KEY = "\"\\$seq\"";
2266
+ const BASE_KEY = "\"\\$base\"";
2267
+ const LISTS_KEY = "\"\\$lists\"";
2268
+ const listElement = (type) => {
2269
+ const inner = type.endsWith("?") ? type.slice(0, -1) : type;
2270
+ return inner.startsWith("List<") && inner.endsWith(">") ? inner.slice(5, -1) : null;
2271
+ };
2272
+ const holders = contract.views.flatMap((view) => [
2273
+ `private class Held${view.name}_${sourceId} {`,
2274
+ ...view.props.flatMap((prop) => {
2275
+ if (prop.callback) {
2276
+ return prop.nativeType.endsWith("?") ? [` var has${cap(prop.name)}: Boolean = false`] : [];
2277
+ }
2278
+ const declared = qualifiedType(prop.nativeType);
2279
+ if (prop.optional || prop.nativeType.endsWith("?")) return [` var ${prop.name}: ${declared} = null`];
2280
+ const zero = unboxedZero(prop.nativeType);
2281
+ if (zero !== null) return [` var ${prop.name}: ${declared} = ${zero}`];
2282
+ if (prop.nativeType === "String") return [` var ${prop.name}: String = ""`];
2283
+ return [` var ${prop.name}: ${declared}? = null`];
2284
+ }),
2285
+ "}"
2286
+ ]);
2287
+ const mergers = contract.views.flatMap((view) => view.props.flatMap((prop) => {
2288
+ if (prop.callback) return [];
2289
+ const elem = listElement(prop.nativeType);
2290
+ if (elem === null) return [];
2291
+ const element = qualifiedType(elem);
2292
+ return [
2293
+ `private fun mergeLists${view.name}${cap(prop.name)}(reader: ${readerClass}, old: List<${element}>?, prop: String): List<${element}>? {`,
2294
+ ` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("lists.$prop: expected object")`,
2295
+ " reader.beginObject()",
2296
+ " var length = -1",
2297
+ ` var set: List<Pair<Int, ${element}>>? = null`,
2298
+ " while (reader.hasNext()) {",
2299
+ " when (reader.selectName(\"length\", \"set\")) {",
2300
+ ` 0 -> length = readInt(reader, "lists.$prop.length")`,
2301
+ ` 1 -> set = readList(reader, "lists.$prop.set") { itemReader, itemPath -> itemReader.beginArray(); val si = readInt(itemReader, itemPath); if (!itemReader.hasNext()) error("$itemPath: expected element"); val sv = ${value(elem, "itemReader", "itemPath")}; itemReader.endArray(); si to sv }`,
2302
+ " else -> reader.skipValue()",
2303
+ " }",
2304
+ " }",
2305
+ " reader.endObject()",
2306
+ " if (length < 0) error(\"lists.$prop: needs length\")",
2307
+ " val current = old ?: return null",
2308
+ " val entries = set ?: emptyList()",
2309
+ " for (j in 1 until entries.size) require(entries[j].first > entries[j - 1].first) { \"lists.$prop: set out of order\" }",
2310
+ ` val fresh = ArrayList<${element}>(length)`,
2311
+ " var s = 0",
2312
+ " for (i in 0 until length) {",
2313
+ " if (s < entries.size && entries[s].first == i) { fresh.add(entries[s].second); s++ }",
2314
+ " else if (i < current.size) fresh.add(current[i])",
2315
+ " else return null",
2316
+ " }",
2317
+ " return fresh",
2318
+ "}"
2319
+ ];
2320
+ }));
1803
2321
  return [
2322
+ ...holders,
2323
+ ...mergers,
2324
+ "",
1804
2325
  `@Keep class OneNativeSourceViews_${sourceId}: OneNativeSourceViewDispatch {`,
1805
2326
  ` override val contractHash = ${JSON.stringify(hash)}`,
2327
+ " override fun apply(view: String, propsJson: String, state: OneNativeSourceKeyedState): Boolean {",
2328
+ ` val reader = ${readerClass}(propsJson)`,
2329
+ ` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("props must be a JSON object")`,
2330
+ " reader.beginObject()",
2331
+ " when (view) {",
2332
+ ...contract.views.flatMap((view) => {
2333
+ const holder = `Held${view.name}_${sourceId}`;
2334
+ const lists = view.props.filter((prop) => !prop.callback && listElement(prop.nativeType) !== null);
2335
+ const options = [
2336
+ SEQ_KEY,
2337
+ BASE_KEY,
2338
+ LISTS_KEY,
2339
+ ...view.props.map((prop) => JSON.stringify(prop.name))
2340
+ ].join(", ");
2341
+ return [
2342
+ ` ${JSON.stringify(view.name)} -> {`,
2343
+ ` var held = state.views[${JSON.stringify(view.name)}] as? ${holder}`,
2344
+ " var seq = -1",
2345
+ " var hasSeq = false",
2346
+ " var base = -1",
2347
+ " var hasBase = false",
2348
+ " var sawLists = false",
2349
+ ...view.props.flatMap((prop) => {
2350
+ if (prop.callback) {
2351
+ return prop.nativeType.endsWith("?") ? [` var ${prop.name}Doc: Boolean? = null`] : [];
2352
+ }
2353
+ const declared = qualifiedType(prop.nativeType);
2354
+ if (prop.optional || prop.nativeType.endsWith("?")) {
2355
+ return [` var ${prop.name}: ${declared} = null`, ` var has${cap(prop.name)} = false`];
2356
+ }
2357
+ const zero = unboxedZero(prop.nativeType);
2358
+ if (zero !== null) return [` var ${prop.name} = ${zero}`, ` var has${cap(prop.name)} = false`];
2359
+ return [` var ${prop.name}: ${declared}? = null`];
2360
+ }),
2361
+ ...lists.map((prop) => ` var ${prop.name}MergeFailed = false`),
2362
+ " while (reader.hasNext()) {",
2363
+ ` when (reader.selectName(${options})) {`,
2364
+ " 0 -> { seq = readInt(reader, \"seq\"); hasSeq = true }",
2365
+ " 1 -> { base = readInt(reader, \"base\"); hasBase = true }",
2366
+ " 2 -> {",
2367
+ " sawLists = true",
2368
+ ` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("lists: expected object")`,
2369
+ " reader.beginObject()",
2370
+ " while (reader.hasNext()) {",
2371
+ ` when (reader.selectName(${lists.map((prop) => JSON.stringify(prop.name)).join(", ")})) {`,
2372
+ ...lists.map((prop, listsIndex) => ` ${listsIndex} -> { val merged${cap(prop.name)} = mergeLists${view.name}${cap(prop.name)}(reader, held?.${prop.name}, ${JSON.stringify(prop.name)}); if (merged${cap(prop.name)} == null) ${prop.name}MergeFailed = true else ${prop.name} = merged${cap(prop.name)} }`),
2373
+ " else -> reader.skipValue()",
2374
+ " }",
2375
+ " }",
2376
+ " reader.endObject()",
2377
+ " }",
2378
+ ...view.props.map((prop, index) => {
2379
+ const arm = index + 3;
2380
+ if (prop.callback) {
2381
+ return prop.nativeType.endsWith("?") ? ` ${arm} -> ${prop.name}Doc = readOptBoolean(reader)` : ` ${arm} -> reader.skipValue()`;
2382
+ }
2383
+ const path = JSON.stringify(`props.${prop.name}`);
2384
+ const read = `${prop.name} = ${value(prop.nativeType, "reader", path)}`;
2385
+ const needsFlag = prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null;
2386
+ return needsFlag ? ` ${arm} -> { ${read}; has${cap(prop.name)} = true }` : ` ${arm} -> ${read}`;
2387
+ }),
2388
+ " else -> reader.skipValue()",
2389
+ " }",
2390
+ " }",
2391
+ " reader.endObject()",
2392
+ " reader.endDocument()",
2393
+ " if (hasBase && !hasSeq) error(\"seq: required\")",
2394
+ " if (sawLists && !hasBase) error(\"lists: needs a change document\")",
2395
+ " if (!hasBase) {",
2396
+ ...view.props.flatMap((prop) => {
2397
+ if (prop.callback || prop.optional) return [];
2398
+ if (prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
2399
+ return [` if (!has${cap(prop.name)}) error("props.${prop.name}: required")`];
2400
+ }
2401
+ return [];
2402
+ }),
2403
+ ` val h = held ?: ${holder}().also { state.views[${JSON.stringify(view.name)}] = it }`,
2404
+ ...view.props.flatMap((prop) => {
2405
+ if (prop.callback) {
2406
+ return prop.nativeType.endsWith("?") ? [` h.has${cap(prop.name)} = ${prop.name}Doc == true`] : [];
2407
+ }
2408
+ if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
2409
+ return [` h.${prop.name} = ${prop.name}`];
2410
+ }
2411
+ return [` h.${prop.name} = (${prop.name} ?: error("props.${prop.name}: required"))`];
2412
+ }),
2413
+ " if (hasSeq) state.seq = seq",
2414
+ " state.revision++",
2415
+ " return true",
2416
+ " }",
2417
+ " if (seq == state.seq) return true",
2418
+ " val h = held",
2419
+ " if (base == state.seq && h != null) {",
2420
+ ...view.props.flatMap((prop) => {
2421
+ if (prop.callback) {
2422
+ return prop.nativeType.endsWith("?") ? [` if (${prop.name}Doc != null) h.has${cap(prop.name)} = ${prop.name}Doc`] : [];
2423
+ }
2424
+ const mergeFailed = listElement(prop.nativeType) !== null ? [` if (${prop.name}MergeFailed) error("lists.${prop.name}: cannot merge")`] : [];
2425
+ if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
2426
+ return [...mergeFailed, ` if (has${cap(prop.name)}) h.${prop.name} = ${prop.name}`];
2427
+ }
2428
+ return [...mergeFailed, ` if (${prop.name} != null) h.${prop.name} = ${prop.name}`];
2429
+ }),
2430
+ " state.seq = seq",
2431
+ " state.revision++",
2432
+ " return true",
2433
+ " }",
2434
+ " return false",
2435
+ " }"
2436
+ ];
2437
+ }),
2438
+ " else -> error(\"composable $view is not linked; rebuild the app\")",
2439
+ " }",
2440
+ " }",
1806
2441
  " @Composable",
1807
- " override fun Content(view: String, propsJson: String, emit: (String, String) -> Unit) {",
1808
- " val props = JSONObject(propsJson)",
2442
+ " override fun Content(view: String, state: OneNativeSourceKeyedState, emit: (String, String) -> Unit) {",
1809
2443
  " when (view) {",
1810
- ...contract.views.flatMap((view) => [
1811
- ` ${JSON.stringify(view.name)} -> ${qualifier}${view.name}(`,
1812
- ...view.props.map((prop) => {
1813
- const path = JSON.stringify(`props.${prop.name}`);
1814
- if (prop.callback) {
1815
- const args = prop.callback.map((_, index) => `a${index}`);
1816
- const types = prop.callback.map((arg) => arg.nativeType);
1817
- const body = `emit(${JSON.stringify(prop.name)}, JSONArray()${args.map((arg, index) => `.put(${encode(types[index], arg)})`).join("")}.toString())`;
1818
- const lambda = `{ ${args.length > 0 ? `${args.join(", ")} -> ` : ""}${body} }`;
1819
- return ` ${prop.name} = ${prop.nativeType.endsWith("?") ? `if (props.optBoolean(${JSON.stringify(prop.name)})) (${lambda}) else null` : lambda},`;
1820
- }
1821
- const input = prop.optional ? `(props.opt(${JSON.stringify(prop.name)}) ?: JSONObject.NULL)` : `(props.opt(${JSON.stringify(prop.name)}) ?: error(${JSON.stringify(`props.${prop.name}: required`)}))`;
1822
- return ` ${prop.name} = ${value(prop.nativeType, input, path)},`;
1823
- }),
1824
- " )"
1825
- ]),
2444
+ ...contract.views.flatMap((view) => {
2445
+ const holder = `Held${view.name}_${sourceId}`;
2446
+ return [
2447
+ ` ${JSON.stringify(view.name)} -> {`,
2448
+ " // the revision read resubscribes this scope on every apply",
2449
+ " val revision = state.revision",
2450
+ ` val held = state.views[${JSON.stringify(view.name)}] as? ${holder}`,
2451
+ " held?.let { h ->",
2452
+ ` ${qualifier}${view.name}(`,
2453
+ ...view.props.map((prop) => {
2454
+ if (prop.callback) {
2455
+ const args = prop.callback.map((_, index) => `a${index}`);
2456
+ const types = prop.callback.map((arg) => arg.nativeType);
2457
+ const body = `emit(${JSON.stringify(prop.name)}, JSONArray()${args.map((arg, index) => `.put(${encode(types[index], arg)})`).join("")}.toString())`;
2458
+ const lambda = `{ ${args.length > 0 ? `${args.join(", ")} -> ` : ""}${body} }`;
2459
+ return ` ${prop.name} = ${prop.nativeType.endsWith("?") ? `if (h.has${cap(prop.name)}) (${lambda}) else null` : lambda},`;
2460
+ }
2461
+ if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null || prop.nativeType === "String") {
2462
+ return ` ${prop.name} = h.${prop.name},`;
2463
+ }
2464
+ return ` ${prop.name} = h.${prop.name}!!,`;
2465
+ }),
2466
+ " )",
2467
+ " }",
2468
+ " }"
2469
+ ];
2470
+ }),
1826
2471
  " else -> error(\"composable $view is not linked; rebuild the app\")",
1827
2472
  " }",
1828
2473
  " }",