vxrn 2.13.10 → 2.14.0

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.
@@ -1562,12 +1562,13 @@ function nativeSourceContract(file, source) {
1562
1562
  }));
1563
1563
  declarations.push(...views.map(function(view) {
1564
1564
  return `export declare function ${view.name}(props: {${view.props.map(function(prop) {
1565
- return ` ${prop.name}${prop.optional ? "?" : ""}: ${prop.type}`;
1566
- }).join(";")} }): ReactElement`;
1565
+ return ` ${prop.name}${prop.optional ? "?" : ""}: ${prop.type};`;
1566
+ }).join("")} style?: StyleProp<ViewStyle> }): ReactElement`;
1567
1567
  }));
1568
1568
  if (defaultView) {
1569
1569
  declarations.unshift("export default function SwiftPackage(props: Record<string, unknown>): ReactElement");
1570
1570
  }
1571
+ if (views.length > 0) declarations.unshift("import type { StyleProp, ViewStyle } from 'react-native'");
1571
1572
  if (defaultView || views.length > 0) declarations.unshift("import type { ReactElement } from 'react'");
1572
1573
  var declaration = `${GENERATED_HEADER}${declarations.join("\n\n")}
1573
1574
  `;
@@ -1939,18 +1940,151 @@ function renderSwiftSourceGlue(packageId, contracts) {
1939
1940
  ];
1940
1941
  };
1941
1942
  var renderViews = function renderViews2() {
1943
+ var notLinked2 = " default: throw NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"swift view \\(name) is not linked; rebuild the app\"])";
1944
+ var fieldsOf = function fieldsOf2(view) {
1945
+ return view.props.filter(function(prop) {
1946
+ return !prop.callback || prop.optional;
1947
+ }).map(function(prop, index) {
1948
+ var type = prop.callback ? "Bool?" : prop.nativeType;
1949
+ var optional = type.endsWith("?");
1950
+ var list = /^\[(.*)\]\??$/.exec(type);
1951
+ return {
1952
+ name: prop.name,
1953
+ type,
1954
+ optional,
1955
+ list: list && !list[1].includes(":") ? list[1] : null,
1956
+ key: `p${index}`
1957
+ };
1958
+ });
1959
+ };
1942
1960
  return [
1943
- " private static func missing(_ path: String) throws -> Any {",
1944
- " throw NSError(domain: \"OneNativeSource\", code: 2, userInfo: [NSLocalizedDescriptionKey: \"\\(path): required\"])",
1961
+ " private struct OneListPatch<E: Decodable>: Decodable {",
1962
+ " let length: Int",
1963
+ " let set: [(Int, E)]",
1964
+ " private struct Entry: Decodable {",
1965
+ " let index: Int",
1966
+ " let value: E",
1967
+ " init(from decoder: Decoder) throws {",
1968
+ " var c = try decoder.unkeyedContainer()",
1969
+ " index = try c.decode(Int.self)",
1970
+ " value = try c.decode(E.self)",
1971
+ " }",
1972
+ " }",
1973
+ " private enum CodingKeys: String, CodingKey { case length, set }",
1974
+ " init(from decoder: Decoder) throws {",
1975
+ " let c = try decoder.container(keyedBy: CodingKeys.self)",
1976
+ " length = try c.decode(Int.self, forKey: .length)",
1977
+ " set = try c.decode([Entry].self, forKey: .set).map { ($0.index, $0.value) }",
1978
+ " }",
1945
1979
  " }",
1946
- " static func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {",
1947
- " guard let props = try JSONSerialization.jsonObject(with: Data(propsJson.utf8)) as? [String: Any] else {",
1948
- " throw NSError(domain: \"OneNativeSource\", code: 2, userInfo: [NSLocalizedDescriptionKey: \"props must be a JSON object\"])",
1980
+ " private nonisolated static func patch<E>(_ list: inout [E], _ patch: OneListPatch<E>) -> Bool {",
1981
+ " if patch.length < 0 { return false }",
1982
+ " if list.count > patch.length { list.removeLast(list.count - patch.length) }",
1983
+ " for (index, value) in patch.set {",
1984
+ " if index < list.count { list[index] = value } else if index == list.count && index < patch.length { list.append(value) } else { return false }",
1949
1985
  " }",
1986
+ " return list.count == patch.length",
1987
+ " }",
1988
+ " private nonisolated static func missing(_ prop: String) -> Error {",
1989
+ " NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"missing prop \\(prop)\"])",
1990
+ " }",
1991
+ ...views.flatMap(function(view) {
1992
+ var fields = fieldsOf(view);
1993
+ var lists = fields.filter(function(field) {
1994
+ return field.list;
1995
+ });
1996
+ return [
1997
+ ...lists.length > 0 ? [
1998
+ ` private struct Lists_${view.name}: Decodable {`,
1999
+ ...lists.map(function(field) {
2000
+ return ` let ${field.key}: OneListPatch<${field.list}>?`;
2001
+ }),
2002
+ ` private enum CodingKeys: String, CodingKey { ${lists.map(function(field) {
2003
+ return `case ${field.key} = ${JSON.stringify(field.name)}`;
2004
+ }).join("; ")} }`,
2005
+ " }"
2006
+ ] : [],
2007
+ ` private struct Doc_${view.name}: Decodable {`,
2008
+ " let seq: Int",
2009
+ " let base: Int?",
2010
+ ...lists.length > 0 ? [` let lists: Lists_${view.name}?`] : [],
2011
+ ...fields.map(function(field) {
2012
+ return ` let ${field.key}: Optional<${field.type}>`;
2013
+ }),
2014
+ ` private enum CodingKeys: String, CodingKey { ${[
2015
+ "case seq = \"$seq\"",
2016
+ "case base = \"$base\"",
2017
+ "case lists = \"$lists\"",
2018
+ ...fields.map(function(field) {
2019
+ return `case ${field.key} = ${JSON.stringify(field.name)}`;
2020
+ })
2021
+ ].join("; ")} }`,
2022
+ " init(from decoder: Decoder) throws {",
2023
+ " let c = try decoder.container(keyedBy: CodingKeys.self)",
2024
+ " seq = try c.decodeIfPresent(Int.self, forKey: .seq) ?? 0",
2025
+ " base = try c.decodeIfPresent(Int.self, forKey: .base)",
2026
+ ...lists.length > 0 ? [` lists = try c.decodeIfPresent(Lists_${view.name}.self, forKey: .lists)`] : [],
2027
+ ...fields.map(function(field) {
2028
+ return ` ${field.key} = try c.contains(.${field.key}) ? Optional<${field.type}>.some(c.decode(${field.type}.self, forKey: .${field.key})) : Optional<${field.type}>.none`;
2029
+ }),
2030
+ " }",
2031
+ " }",
2032
+ ` private final class Held_${view.name} {`,
2033
+ " var oneSeq: Int",
2034
+ ...fields.map(function(field) {
2035
+ return ` var \`${field.name}\`: ${field.type}`;
2036
+ }),
2037
+ ` init(_ d: Doc_${view.name}) throws {`,
2038
+ " oneSeq = d.seq",
2039
+ ...fields.flatMap(function(field) {
2040
+ return 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}`];
2041
+ }),
2042
+ " }",
2043
+ ` func apply(_ d: Doc_${view.name}) -> Bool {`,
2044
+ " if d.base != oneSeq { return false }",
2045
+ ...lists.flatMap(function(field) {
2046
+ return [
2047
+ ` var ${field.key} = \`${field.name}\``,
2048
+ ` if let v = d.${field.key} { ${field.key} = v }`,
2049
+ ` if let p = d.lists?.${field.key} {`,
2050
+ ...field.optional ? [
2051
+ ` guard var items = ${field.key} else { return false }`,
2052
+ ` if !${bodyName}.patch(&items, p) { return false }`,
2053
+ ` ${field.key} = items`
2054
+ ] : [` if !${bodyName}.patch(&${field.key}, p) { return false }`],
2055
+ " }"
2056
+ ];
2057
+ }),
2058
+ ...fields.map(function(field) {
2059
+ return field.list ? ` \`${field.name}\` = ${field.key}` : ` if let v = d.${field.key} { \`${field.name}\` = v }`;
2060
+ }),
2061
+ " oneSeq = d.seq",
2062
+ " return true",
2063
+ " }",
2064
+ " }"
2065
+ ];
2066
+ }),
2067
+ " static func update(_ held: AnyObject?, _ name: String, _ text: String) throws -> AnyObject? {",
2068
+ " let data = Data(text.utf8)",
1950
2069
  " switch name {",
1951
2070
  ...views.flatMap(function(view) {
1952
2071
  return [
1953
2072
  ` case ${JSON.stringify(view.name)}:`,
2073
+ ` let doc = try decode(Doc_${view.name}.self, from: data)`,
2074
+ ` if doc.base == nil { return try Held_${view.name}(doc) }`,
2075
+ ` guard let h = held as? Held_${view.name}, h.apply(doc) else { return nil }`,
2076
+ " return h"
2077
+ ];
2078
+ }),
2079
+ notLinked2,
2080
+ " }",
2081
+ " }",
2082
+ " static func build(_ held: AnyObject, _ name: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {",
2083
+ " switch name {",
2084
+ ...views.flatMap(function(view) {
2085
+ return [
2086
+ ` case ${JSON.stringify(view.name)}:`,
2087
+ ` 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)"]) }`,
1954
2088
  ` return AnyView(${view.name}(`,
1955
2089
  ...view.props.map(function(prop, index) {
1956
2090
  var comma = index === view.props.length - 1 ? "" : ",";
@@ -1962,16 +2096,19 @@ function renderSwiftSourceGlue(packageId, contracts) {
1962
2096
  var lambda = `{ (${params}) -> Void in emit(${key}, args([${prop.callback.map(function(_, n) {
1963
2097
  return `a${n}`;
1964
2098
  }).join(", ")}])) }`;
1965
- return ` ${prop.name}: ${prop.optional ? `(props[${key}] as? Bool) == true ? ${lambda} : nil` : lambda}${comma}`;
2099
+ return ` ${prop.name}: ${prop.optional ? `props.\`${prop.name}\` == true ? ${lambda} : nil` : lambda}${comma}`;
1966
2100
  }
1967
- var input = prop.optional ? `props[${key}] ?? NSNull()` : `props[${key}] ?? missing(${JSON.stringify(`props.${prop.name}`)})`;
1968
- return ` ${prop.name}: try decode(${input}, as: ${prop.nativeType}.self)${comma}`;
2101
+ return ` ${prop.name}: props.\`${prop.name}\`${comma}`;
1969
2102
  }),
1970
2103
  " ))"
1971
2104
  ];
1972
2105
  }),
1973
- " default: throw NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"swift view \\(name) is not linked; rebuild the app\"])",
2106
+ notLinked2,
1974
2107
  " }",
2108
+ " }",
2109
+ " static func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView {",
2110
+ " guard let held = try update(nil, name, propsJson) else { throw NSError(domain: \"OneNativeSource\", code: 1, userInfo: [NSLocalizedDescriptionKey: \"\\(name) needs a full props document\"]) }",
2111
+ " return try build(held, name, emit)",
1975
2112
  " }"
1976
2113
  ];
1977
2114
  };
@@ -2042,13 +2179,15 @@ function renderSwiftSourceGlue(packageId, contracts) {
2042
2179
  " if let date = (try? isoDateFractional.parse(text)) ?? (try? isoDate.parse(text)) { return date }",
2043
2180
  " throw DecodingError.dataCorruptedError(in: container, debugDescription: \"expected ISO date\")",
2044
2181
  " }",
2045
- " private static func decode<T: Decodable>(_ value: Any, as type: T.Type) throws -> T {",
2046
- " let data = try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed])",
2182
+ " private static func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {",
2047
2183
  " let decoder = JSONDecoder()",
2048
2184
  " decoder.dateDecodingStrategy = .custom { try decodeDate($0) }",
2049
2185
  " decoder.dataDecodingStrategy = .base64",
2050
2186
  " return try decoder.decode(T.self, from: data)",
2051
2187
  " }",
2188
+ " private static func decode<T: Decodable>(_ value: Any, as type: T.Type) throws -> T {",
2189
+ " try decode(T.self, from: try JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed]))",
2190
+ " }",
2052
2191
  " private static func encode(_ value: any Encodable) throws -> String {",
2053
2192
  " let encoder = JSONEncoder()",
2054
2193
  " encoder.dateEncodingStrategy = .iso8601",
@@ -2116,6 +2255,8 @@ function renderSwiftSourceGlue(packageId, contracts) {
2116
2255
  " public required override init() {}",
2117
2256
  ` public var contractHash: String { ${bodyName}.hash }`,
2118
2257
  ` public func view(_ name: String, _ propsJson: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView { try ${bodyName}.view(name, propsJson, emit) }`,
2258
+ ` public func updateView(_ held: AnyObject?, _ name: String, _ document: String) throws -> AnyObject? { try ${bodyName}.update(held, name, document) }`,
2259
+ ` public func buildView(_ held: AnyObject, _ name: String, _ emit: @escaping (String, String) -> Void) throws -> AnyView { try ${bodyName}.build(held, name, emit) }`,
2119
2260
  "}"
2120
2261
  ] : [],
2121
2262
  "#endif",
@@ -2126,6 +2267,14 @@ function renderSwiftSourceGlue(packageId, contracts) {
2126
2267
  source
2127
2268
  };
2128
2269
  }
2270
+ function unboxedZero(type) {
2271
+ if (type === "Int") return "0";
2272
+ if (type === "Long") return "0L";
2273
+ if (type === "Double") return "0.0";
2274
+ if (type === "Float") return "0f";
2275
+ if (type === "Boolean") return "false";
2276
+ return null;
2277
+ }
2129
2278
  function renderKotlinSourceGlue(sourceId, contract) {
2130
2279
  if (contract.language !== "kotlin") throw new Error("kotlin glue received a swift contract");
2131
2280
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(sourceId)) throw new Error(`invalid kotlin source id ${sourceId}`);
@@ -2133,6 +2282,8 @@ function renderKotlinSourceGlue(sourceId, contract) {
2133
2282
  sourceId,
2134
2283
  contract: contract.hash
2135
2284
  })).digest("hex");
2285
+ var readerClass = `OneJsonReader_${sourceId}`;
2286
+ var tokenClass = `OneJsonToken_${sourceId}`;
2136
2287
  var structsByName = new Map(contract.structs.map(function(entry) {
2137
2288
  return [entry.name, entry];
2138
2289
  }));
@@ -2145,6 +2296,23 @@ function renderKotlinSourceGlue(sourceId, contract) {
2145
2296
  var isDateType = function isDateType2(type) {
2146
2297
  return type === "Date" || type === "java.util.Date";
2147
2298
  };
2299
+ var qualifiedType = function qualifiedType1(type) {
2300
+ if (type.endsWith("?")) return `${qualifiedType(type.slice(0, -1))}?`;
2301
+ if (type.startsWith("List<") && type.endsWith(">")) return `List<${qualifiedType(type.slice(5, -1))}>`;
2302
+ if (type.startsWith("Map<String,") && type.endsWith(">")) return `Map<String, ${qualifiedType(type.slice(11, -1))}>`;
2303
+ if (structsByName.has(type) || enumsByName.has(type)) return qualified(type);
2304
+ if (isDateType(type)) return "java.util.Date";
2305
+ if ([
2306
+ "String",
2307
+ "Boolean",
2308
+ "Double",
2309
+ "Float",
2310
+ "Int",
2311
+ "Long",
2312
+ "ByteArray"
2313
+ ].includes(type)) return type;
2314
+ throw new Error(`unsupported kotlin glue type ${type}`);
2315
+ };
2148
2316
  var mentions = function mentions1(type, match) {
2149
2317
  var rest = type;
2150
2318
  while (rest.endsWith("?")) rest = rest.slice(0, -1);
@@ -2196,25 +2364,25 @@ function renderKotlinSourceGlue(sourceId, contract) {
2196
2364
  return leaf === "ByteArray";
2197
2365
  });
2198
2366
  });
2199
- var value = function value1(type, input, path) {
2367
+ var value = function value1(type, reader, path) {
2200
2368
  if (type.endsWith("?")) {
2201
- return `(${input}).let { raw -> if (raw == null || raw == JSONObject.NULL) null else ${value(type.slice(0, -1), "raw", path)} }`;
2202
- }
2203
- if (type === "String") return `readString(${input}, ${path})`;
2204
- if (type === "Boolean") return `readBoolean(${input}, ${path})`;
2205
- if (type === "Double") return `readDouble(${input}, ${path})`;
2206
- if (type === "Float") return `readDouble(${input}, ${path}).toFloat()`;
2207
- if (type === "Int") return `readInt(${input}, ${path})`;
2208
- if (type === "Long") return `readLong(${input}, ${path})`;
2209
- if (isDateType(type)) return `readDate(${input}, ${path})`;
2210
- if (type === "ByteArray") return `readData(${input}, ${path})`;
2211
- if (structsByName.has(type)) return `read${type}(${input}, ${path})`;
2212
- if (enumsByName.has(type)) return `read${type}(${input}, ${path})`;
2369
+ return `(if (${reader}.peek() == ${tokenClass}.NULL) { ${reader}.nextNull(); null } else ${value(type.slice(0, -1), reader, path)})`;
2370
+ }
2371
+ if (type === "String") return `readString(${reader}, ${path})`;
2372
+ if (type === "Boolean") return `readBoolean(${reader}, ${path})`;
2373
+ if (type === "Double") return `readDouble(${reader}, ${path})`;
2374
+ if (type === "Float") return `readDouble(${reader}, ${path}).toFloat()`;
2375
+ if (type === "Int") return `readInt(${reader}, ${path})`;
2376
+ if (type === "Long") return `readLong(${reader}, ${path})`;
2377
+ if (isDateType(type)) return `readDate(${reader}, ${path})`;
2378
+ if (type === "ByteArray") return `readData(${reader}, ${path})`;
2379
+ if (structsByName.has(type)) return `read${type}(${reader}, ${path})`;
2380
+ if (enumsByName.has(type)) return `read${type}(${reader}, ${path})`;
2213
2381
  if (type.startsWith("List<") && type.endsWith(">")) {
2214
- return `readList(${input}, ${path}) { item, itemPath -> ${value(type.slice(5, -1), "item", "itemPath")} }`;
2382
+ return `readList(${reader}, ${path}) { itemReader, itemPath -> ${value(type.slice(5, -1), "itemReader", "itemPath")} }`;
2215
2383
  }
2216
2384
  if (type.startsWith("Map<String,") && type.endsWith(">")) {
2217
- return `readMap(${input}, ${path}) { item, itemPath -> ${value(type.slice(11, -1), "item", "itemPath")} }`;
2385
+ return `readMap(${reader}, ${path}) { itemReader, itemPath -> ${value(type.slice(11, -1), "itemReader", "itemPath")} }`;
2218
2386
  }
2219
2387
  throw new Error(`unsupported kotlin glue type ${type}`);
2220
2388
  };
@@ -2234,17 +2402,45 @@ function renderKotlinSourceGlue(sourceId, contract) {
2234
2402
  }
2235
2403
  return `JSONObject.wrap(${accessor})`;
2236
2404
  };
2405
+ var cap = function cap2(name) {
2406
+ return name.slice(0, 1).toUpperCase() + name.slice(1);
2407
+ };
2237
2408
  var structHelpers = contract.structs.flatMap(function(struct) {
2238
2409
  return [
2239
- `private fun read${struct.name}(value: Any, path: String): ${qualified(struct.name)} {`,
2240
- " val obj = value as? JSONObject ?: error(\"$path: expected object\")",
2410
+ `private fun read${struct.name}(reader: ${readerClass}, path: String): ${qualified(struct.name)} {`,
2411
+ ` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("$path: expected object")`,
2412
+ " reader.beginObject()",
2413
+ ...struct.fields.flatMap(function(field) {
2414
+ var declared = qualifiedType(field.nativeType);
2415
+ if (field.optional) return [` var ${field.name}: ${declared} = ${field.fallback}`];
2416
+ var zero = unboxedZero(field.nativeType);
2417
+ if (zero !== null) return [` var ${field.name} = ${zero}`, ` var has${cap(field.name)} = false`];
2418
+ var boxed = declared.endsWith("?") ? declared : `${declared}?`;
2419
+ return [` var ${field.name}: ${boxed} = null`, ...field.nativeType.endsWith("?") ? [` var has${cap(field.name)} = false`] : []];
2420
+ }),
2421
+ " while (reader.hasNext()) {",
2422
+ ` when (reader.selectName(${struct.fields.map(function(field) {
2423
+ return JSON.stringify(field.name);
2424
+ }).join(", ")})) {`,
2425
+ ...struct.fields.map(function(field, index) {
2426
+ var fieldPath = `"$path.${field.name}"`;
2427
+ var read = `${field.name} = ${value(field.nativeType, "reader", fieldPath)}`;
2428
+ var needsFlag = !field.optional && (field.nativeType.endsWith("?") || unboxedZero(field.nativeType) !== null);
2429
+ return needsFlag ? ` ${index} -> { ${read}; has${cap(field.name)} = true }` : ` ${index} -> ${read}`;
2430
+ }),
2431
+ " else -> reader.skipValue()",
2432
+ " }",
2433
+ " }",
2434
+ " reader.endObject()",
2435
+ ...struct.fields.flatMap(function(field) {
2436
+ return !field.optional && unboxedZero(field.nativeType) !== null ? [` if (!has${cap(field.name)}) error("$path.${field.name}: required")`] : [];
2437
+ }),
2241
2438
  ` return ${qualified(struct.name)}(`,
2242
2439
  ...struct.fields.map(function(field) {
2243
- var _field_fallback;
2244
- var fieldPath = `"$path.${field.name}"`;
2245
- var required = `error("$path.${field.name}: required")`;
2246
- var input = field.optional ? `(if (obj.has(${JSON.stringify(field.name)})) obj.get(${JSON.stringify(field.name)}) else ${(_field_fallback = field.fallback) !== null && _field_fallback !== void 0 ? _field_fallback : required})` : `(obj.opt(${JSON.stringify(field.name)}) ?: ${required})`;
2247
- return ` ${field.name} = ${value(field.nativeType, input, fieldPath)},`;
2440
+ if (field.optional) return ` ${field.name} = ${field.name},`;
2441
+ if (field.nativeType.endsWith("?")) return ` ${field.name} = (if (has${cap(field.name)}) ${field.name} else error("$path.${field.name}: required")),`;
2442
+ if (unboxedZero(field.nativeType) !== null) return ` ${field.name} = ${field.name},`;
2443
+ return ` ${field.name} = (${field.name} ?: error("$path.${field.name}: required")),`;
2248
2444
  }),
2249
2445
  " )",
2250
2446
  "}",
@@ -2258,8 +2454,8 @@ function renderKotlinSourceGlue(sourceId, contract) {
2258
2454
  });
2259
2455
  var enumHelpers = contract.enums.flatMap(function(entry) {
2260
2456
  return [
2261
- `private fun read${entry.name}(value: Any, path: String): ${qualified(entry.name)} {`,
2262
- " val text = readString(value, path)",
2457
+ `private fun read${entry.name}(reader: ${readerClass}, path: String): ${qualified(entry.name)} {`,
2458
+ " val text = readString(reader, path)",
2263
2459
  ` return try { ${qualified(entry.name)}.valueOf(text) } catch (e: IllegalArgumentException) { error("$path: expected one of ${entry.cases.map(function(item) {
2264
2460
  return item.name;
2265
2461
  }).join(", ")}") }`,
@@ -2268,40 +2464,322 @@ function renderKotlinSourceGlue(sourceId, contract) {
2268
2464
  });
2269
2465
  var source = [
2270
2466
  "import androidx.annotation.Keep",
2271
- ...contract.views.length === 0 ? [] : ["import androidx.compose.runtime.Composable", "import dev.onejs.one.OneNativeSourceViewDispatch"],
2467
+ ...contract.views.length === 0 ? [] : [
2468
+ "import androidx.compose.runtime.Composable",
2469
+ "import dev.onejs.one.OneNativeSourceKeyedState",
2470
+ "import dev.onejs.one.OneNativeSourceViewDispatch"
2471
+ ],
2272
2472
  "import dev.onejs.one.OneNativeSourceDispatch",
2273
2473
  ...needsEvents ? ["import dev.onejs.one.source.OneNativeSourceEvents"] : [],
2274
2474
  "import org.json.JSONArray",
2275
2475
  "import org.json.JSONObject",
2276
2476
  "",
2277
- "private fun readString(value: Any, path: String): String = value as? String ?: error(\"$path: expected string\")",
2278
- "private fun readBoolean(value: Any, path: String): Boolean = value as? Boolean ?: error(\"$path: expected boolean\")",
2279
- "private fun readDouble(value: Any, path: String): Double {",
2280
- " val result = (value as? Number)?.toDouble() ?: error(\"$path: expected number\")",
2477
+ `private enum class ${tokenClass} { BEGIN_OBJECT, END_OBJECT, BEGIN_ARRAY, END_ARRAY, STRING, NUMBER, BOOLEAN, NULL }`,
2478
+ "",
2479
+ "// a forward-only json reader over the props text: values decode straight",
2480
+ "// into typed props in one pass with no intermediate JSONObject. pure",
2481
+ "// kotlin with no java or android imports, so the same glue compiles for",
2482
+ "// kotlin/wasm.",
2483
+ `private class ${readerClass}(private val text: String) {`,
2484
+ " private var pos = 0",
2485
+ "",
2486
+ " private fun skipSpace() {",
2487
+ " while (pos < text.length) {",
2488
+ " when (text[pos]) {",
2489
+ " ' ', '\\t', '\\n', '\\r' -> pos++",
2490
+ " else -> return",
2491
+ " }",
2492
+ " }",
2493
+ " }",
2494
+ "",
2495
+ " private fun fail(message: String): Nothing = throw IllegalStateException(\"$message near offset $pos\")",
2496
+ "",
2497
+ ` fun peek(): ${tokenClass} {`,
2498
+ " skipSpace()",
2499
+ " if (pos >= text.length) fail(\"unexpected end of json\")",
2500
+ " return when (text[pos]) {",
2501
+ ` '{' -> ${tokenClass}.BEGIN_OBJECT`,
2502
+ ` '}' -> ${tokenClass}.END_OBJECT`,
2503
+ ` '[' -> ${tokenClass}.BEGIN_ARRAY`,
2504
+ ` ']' -> ${tokenClass}.END_ARRAY`,
2505
+ ` '"' -> ${tokenClass}.STRING`,
2506
+ ` 't', 'f' -> ${tokenClass}.BOOLEAN`,
2507
+ ` 'n' -> ${tokenClass}.NULL`,
2508
+ ` '-', in '0'..'9' -> ${tokenClass}.NUMBER`,
2509
+ " else -> fail(\"unexpected character\")",
2510
+ " }",
2511
+ " }",
2512
+ "",
2513
+ " private fun expect(char: Char) {",
2514
+ " skipSpace()",
2515
+ ` if (pos >= text.length || text[pos] != char) fail("expected '$char'")`,
2516
+ " pos++",
2517
+ " }",
2518
+ "",
2519
+ " fun beginObject() = expect('{')",
2520
+ " fun endObject() = expect('}')",
2521
+ " fun beginArray() = expect('[')",
2522
+ " fun endArray() = expect(']')",
2523
+ "",
2524
+ " // inside an object or array: true when another value follows, consuming",
2525
+ " // a comma first; false at the closing bracket.",
2526
+ " fun hasNext(): Boolean {",
2527
+ " skipSpace()",
2528
+ " if (pos >= text.length) fail(\"unexpected end of json\")",
2529
+ " if (text[pos] == ',') {",
2530
+ " pos++",
2531
+ " return true",
2532
+ " }",
2533
+ " return text[pos] != '}' && text[pos] != ']'",
2534
+ " }",
2535
+ "",
2536
+ " fun nextName(): String {",
2537
+ " skipSpace()",
2538
+ ` if (pos >= text.length || text[pos] != '"') fail("expected a name")`,
2539
+ " val name = readString()",
2540
+ " skipSpace()",
2541
+ ` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
2542
+ " pos++",
2543
+ " return name",
2544
+ " }",
2545
+ "",
2546
+ " fun nextString(): String {",
2547
+ " skipSpace()",
2548
+ ` if (pos >= text.length || text[pos] != '"') fail("expected a string")`,
2549
+ " return readString()",
2550
+ " }",
2551
+ "",
2552
+ " // matches an object name against the expected keys without allocating",
2553
+ " // it: the index of the match, or -1 for an unknown key. the name and",
2554
+ " // colon are consumed either way, so the caller skips the value on -1.",
2555
+ " fun selectName(vararg options: String): Int {",
2556
+ " skipSpace()",
2557
+ ` if (pos >= text.length || text[pos] != '"') fail("expected a name")`,
2558
+ " var end = pos + 1",
2559
+ ` while (end < text.length && text[end] != '"' && text[end] != '\\\\') end++`,
2560
+ " if (end < text.length && text[end] == '\\\\') {",
2561
+ " val decoded = readString()",
2562
+ " skipSpace()",
2563
+ ` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
2564
+ " pos++",
2565
+ " return options.indexOf(decoded)",
2566
+ " }",
2567
+ " if (end >= text.length) fail(\"unterminated string\")",
2568
+ " val length = end - pos - 1",
2569
+ " var index = -1",
2570
+ " for (i in options.indices) {",
2571
+ " if (options[i].length == length && text.regionMatches(pos + 1, options[i], 0, length)) { index = i; break }",
2572
+ " }",
2573
+ " pos = end + 1",
2574
+ " skipSpace()",
2575
+ ` if (pos >= text.length || text[pos] != ':') fail("expected ':'")`,
2576
+ " pos++",
2577
+ " return index",
2578
+ " }",
2579
+ "",
2580
+ " private fun readString(): String {",
2581
+ " pos++ // the opening quote",
2582
+ " var end = pos",
2583
+ ` while (end < text.length && text[end] != '"' && text[end] != '\\\\') end++`,
2584
+ ` if (end < text.length && text[end] == '"') {`,
2585
+ " val out = text.substring(pos, end)",
2586
+ " pos = end + 1",
2587
+ " return out",
2588
+ " }",
2589
+ " val out = StringBuilder(text.substring(pos, end))",
2590
+ " pos = end",
2591
+ " while (true) {",
2592
+ " if (pos >= text.length) fail(\"unterminated string\")",
2593
+ " val char = text[pos++]",
2594
+ ` if (char == '"') return out.toString()`,
2595
+ " if (char != '\\\\') {",
2596
+ " out.append(char)",
2597
+ " continue",
2598
+ " }",
2599
+ " if (pos >= text.length) fail(\"unterminated escape\")",
2600
+ " when (text[pos++]) {",
2601
+ ` '"', '\\\\', '/' -> out.append(text[pos - 1])`,
2602
+ " 'b' -> out.append('\\b')",
2603
+ " 'f' -> out.append('\\u000C')",
2604
+ " 'n' -> out.append('\\n')",
2605
+ " 'r' -> out.append('\\r')",
2606
+ " 't' -> out.append('\\t')",
2607
+ " 'u' -> {",
2608
+ " if (pos + 4 > text.length) fail(\"bad unicode escape\")",
2609
+ " var code = 0",
2610
+ " for (i in 0 until 4) {",
2611
+ " val digit = text[pos + i].lowercaseChar()",
2612
+ ` val value = if (digit in '0'..'9') digit - '0' else if (digit in 'a'..'f') digit - 'a' + 10 else fail("bad unicode escape")`,
2613
+ " code = code * 16 + value",
2614
+ " }",
2615
+ " pos += 4",
2616
+ " out.append(code.toChar())",
2617
+ " }",
2618
+ " else -> fail(\"bad escape\")",
2619
+ " }",
2620
+ " }",
2621
+ " }",
2622
+ "",
2623
+ " fun nextDouble(): Double {",
2624
+ " skipSpace()",
2625
+ " val start = pos",
2626
+ " while (pos < text.length && (text[pos] in '0'..'9' || text[pos] == '.' || text[pos] == 'e' || text[pos] == 'E' || text[pos] == '+' || text[pos] == '-')) pos++",
2627
+ " if (pos == start) fail(\"expected a number\")",
2628
+ " return text.substring(start, pos).toDoubleOrNull() ?: fail(\"bad number\")",
2629
+ " }",
2630
+ "",
2631
+ " // plain-digit fast paths: the value when the token at pos is an integer",
2632
+ " // in range, null when it needs the double slow path. nothing is",
2633
+ " // consumed on null, so the caller falls back to nextDouble.",
2634
+ " fun nextIntOrNull(): Int? {",
2635
+ " var i = pos",
2636
+ " val negative = i < text.length && text[i] == '-'",
2637
+ " if (negative) i++",
2638
+ " var value = 0L",
2639
+ " val digits = i",
2640
+ " while (i < text.length && text[i] in '0'..'9') {",
2641
+ " value = value * 10 + (text[i] - '0')",
2642
+ " if (value > 2147483648L) return null",
2643
+ " i++",
2644
+ " }",
2645
+ " if (i == digits) return null",
2646
+ " if (i < text.length && (text[i] == '.' || text[i] == 'e' || text[i] == 'E')) return null",
2647
+ " if (!negative && value > 2147483647L) return null",
2648
+ " pos = i",
2649
+ " return if (negative) -value.toInt() else value.toInt()",
2650
+ " }",
2651
+ "",
2652
+ " fun nextLongOrNull(): Long? {",
2653
+ " var i = pos",
2654
+ " val negative = i < text.length && text[i] == '-'",
2655
+ " if (negative) i++",
2656
+ " var value = 0L",
2657
+ " val digits = i",
2658
+ " while (i < text.length && text[i] in '0'..'9') {",
2659
+ " val digit = (text[i] - '0').toLong()",
2660
+ " if (value > (9007199254740991L - digit) / 10) return null",
2661
+ " value = value * 10 + digit",
2662
+ " i++",
2663
+ " }",
2664
+ " if (i == digits) return null",
2665
+ " if (i < text.length && (text[i] == '.' || text[i] == 'e' || text[i] == 'E')) return null",
2666
+ " pos = i",
2667
+ " return if (negative) -value else value",
2668
+ " }",
2669
+ "",
2670
+ " fun nextBoolean(): Boolean {",
2671
+ " skipSpace()",
2672
+ " if (text.startsWith(\"true\", pos)) {",
2673
+ " pos += 4",
2674
+ " return true",
2675
+ " }",
2676
+ " if (text.startsWith(\"false\", pos)) {",
2677
+ " pos += 5",
2678
+ " return false",
2679
+ " }",
2680
+ " fail(\"expected a boolean\")",
2681
+ " }",
2682
+ "",
2683
+ " fun nextNull() {",
2684
+ " skipSpace()",
2685
+ " if (!text.startsWith(\"null\", pos)) fail(\"expected null\")",
2686
+ " pos += 4",
2687
+ " }",
2688
+ "",
2689
+ " fun skipValue() {",
2690
+ " when (peek()) {",
2691
+ ` ${tokenClass}.BEGIN_OBJECT -> {`,
2692
+ " beginObject()",
2693
+ " while (hasNext()) {",
2694
+ " nextName()",
2695
+ " skipValue()",
2696
+ " }",
2697
+ " endObject()",
2698
+ " }",
2699
+ ` ${tokenClass}.BEGIN_ARRAY -> {`,
2700
+ " beginArray()",
2701
+ " while (hasNext()) {",
2702
+ " skipValue()",
2703
+ " }",
2704
+ " endArray()",
2705
+ " }",
2706
+ ` ${tokenClass}.STRING -> nextString()`,
2707
+ ` ${tokenClass}.NUMBER -> nextDouble()`,
2708
+ ` ${tokenClass}.BOOLEAN -> nextBoolean()`,
2709
+ ` ${tokenClass}.NULL -> nextNull()`,
2710
+ " else -> fail(\"unexpected value\")",
2711
+ " }",
2712
+ " }",
2713
+ "",
2714
+ " fun endDocument() {",
2715
+ " skipSpace()",
2716
+ " if (pos != text.length) fail(\"trailing characters\")",
2717
+ " }",
2718
+ "}",
2719
+ "",
2720
+ `private fun readString(reader: ${readerClass}, path: String): String {`,
2721
+ ` if (reader.peek() != ${tokenClass}.STRING) error("$path: expected string")`,
2722
+ " return reader.nextString()",
2723
+ "}",
2724
+ `private fun readBoolean(reader: ${readerClass}, path: String): Boolean {`,
2725
+ ` if (reader.peek() != ${tokenClass}.BOOLEAN) error("$path: expected boolean")`,
2726
+ " return reader.nextBoolean()",
2727
+ "}",
2728
+ `private fun readDouble(reader: ${readerClass}, path: String): Double {`,
2729
+ ` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
2730
+ " val result = reader.nextDouble()",
2281
2731
  " if (!result.isFinite()) error(\"$path: expected finite number\")",
2282
2732
  " return result",
2283
2733
  "}",
2284
- "private fun readInt(value: Any, path: String): Int {",
2285
- " val result = readDouble(value, path)",
2734
+ `private fun readInt(reader: ${readerClass}, path: String): Int {`,
2735
+ ` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
2736
+ " reader.nextIntOrNull()?.let { return it }",
2737
+ " val result = readDouble(reader, path)",
2286
2738
  " if (result % 1.0 != 0.0 || result < Int.MIN_VALUE || result > Int.MAX_VALUE) error(\"$path: expected Int\")",
2287
2739
  " return result.toInt()",
2288
2740
  "}",
2289
- "private fun readLong(value: Any, path: String): Long {",
2290
- " val result = readDouble(value, path)",
2741
+ `private fun readLong(reader: ${readerClass}, path: String): Long {`,
2742
+ ` if (reader.peek() != ${tokenClass}.NUMBER) error("$path: expected number")`,
2743
+ " reader.nextLongOrNull()?.let { return it }",
2744
+ " val result = readDouble(reader, path)",
2291
2745
  " if (result % 1.0 != 0.0 || result < -9007199254740991.0 || result > 9007199254740991.0) error(\"$path: expected safe Long\")",
2292
2746
  " return result.toLong()",
2293
2747
  "}",
2294
- "private fun <T> readList(value: Any, path: String, read: (Any, String) -> T): List<T> {",
2295
- " val array = value as? JSONArray ?: error(\"$path: expected array\")",
2296
- " return (0 until array.length()).map { index -> read(array.get(index), \"$path[$index]\") }",
2748
+ "// the host sends true for each callback the element passed; any other",
2749
+ "// value reads as absent.",
2750
+ `private fun readOptBoolean(reader: ${readerClass}): Boolean {`,
2751
+ ` if (reader.peek() != ${tokenClass}.BOOLEAN) {`,
2752
+ " reader.skipValue()",
2753
+ " return false",
2754
+ " }",
2755
+ " return reader.nextBoolean()",
2756
+ "}",
2757
+ `private fun <T> readList(reader: ${readerClass}, path: String, read: (${readerClass}, String) -> T): List<T> {`,
2758
+ ` if (reader.peek() != ${tokenClass}.BEGIN_ARRAY) error("$path: expected array")`,
2759
+ " reader.beginArray()",
2760
+ " val result = mutableListOf<T>()",
2761
+ " var index = 0",
2762
+ " while (reader.hasNext()) {",
2763
+ " result.add(read(reader, \"$path[$index]\"))",
2764
+ " index++",
2765
+ " }",
2766
+ " reader.endArray()",
2767
+ " return result",
2297
2768
  "}",
2298
- "private fun <T> readMap(value: Any, path: String, read: (Any, String) -> T): Map<String, T> {",
2299
- " val objectValue = value as? JSONObject ?: error(\"$path: expected object\")",
2300
- " return objectValue.keys().asSequence().associateWith { key -> read(objectValue.get(key), \"$path.$key\") }",
2769
+ `private fun <T> readMap(reader: ${readerClass}, path: String, read: (${readerClass}, String) -> T): Map<String, T> {`,
2770
+ ` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("$path: expected object")`,
2771
+ " reader.beginObject()",
2772
+ " val result = mutableMapOf<String, T>()",
2773
+ " while (reader.hasNext()) {",
2774
+ " val key = reader.nextName()",
2775
+ " result[key] = read(reader, \"$path.$key\")",
2776
+ " }",
2777
+ " reader.endObject()",
2778
+ " return result",
2301
2779
  "}",
2302
2780
  ...needsDate ? [
2303
- "private fun readDate(value: Any, path: String): java.util.Date {",
2304
- " val text = readString(value, path)",
2781
+ `private fun readDate(reader: ${readerClass}, path: String): java.util.Date {`,
2782
+ " val text = readString(reader, path)",
2305
2783
  ` for (pattern in arrayOf("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "yyyy-MM-dd'T'HH:mm:ss'Z'")) {`,
2306
2784
  " try {",
2307
2785
  " return java.text.SimpleDateFormat(pattern, java.util.Locale.US).apply { timeZone = java.util.TimeZone.getTimeZone(\"UTC\"); isLenient = false }.parse(text) ?: error(\"$path: expected date\")",
@@ -2313,9 +2791,9 @@ function renderKotlinSourceGlue(sourceId, contract) {
2313
2791
  ` 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)`
2314
2792
  ] : [],
2315
2793
  ...needsData ? [
2316
- "private fun readData(value: Any, path: String): ByteArray {",
2794
+ `private fun readData(reader: ${readerClass}, path: String): ByteArray {`,
2317
2795
  " try {",
2318
- " return android.util.Base64.decode(readString(value, path), android.util.Base64.NO_WRAP)",
2796
+ " return android.util.Base64.decode(readString(reader, path), android.util.Base64.NO_WRAP)",
2319
2797
  " } catch (decodeError: IllegalArgumentException) {",
2320
2798
  " error(\"$path: expected base64 data\")",
2321
2799
  " }",
@@ -2327,7 +2805,9 @@ function renderKotlinSourceGlue(sourceId, contract) {
2327
2805
  `@Keep class OneNativeSource_${sourceId}: OneNativeSourceDispatch {`,
2328
2806
  ` override val contractHash = ${JSON.stringify(hash)}`,
2329
2807
  " override suspend fun call(module: String, method: String, argsJson: String): String {",
2330
- " val args = JSONArray(argsJson)",
2808
+ ` val reader = ${readerClass}(argsJson)`,
2809
+ ` if (reader.peek() != ${tokenClass}.BEGIN_ARRAY) error("args must be a JSON array")`,
2810
+ " reader.beginArray()",
2331
2811
  " return when (module) {",
2332
2812
  ...contract.modules.flatMap(function(module) {
2333
2813
  return [
@@ -2343,16 +2823,24 @@ function renderKotlinSourceGlue(sourceId, contract) {
2343
2823
  }).join("")}.toString()) }`;
2344
2824
  return [
2345
2825
  ` ${JSON.stringify(method.name)} -> {`,
2346
- ` require(args.length() == 0) { "${sourceId}.${module.name}.${method.name} subscribes; it takes no arguments" }`,
2826
+ ` require(!reader.hasNext()) { "${sourceId}.${module.name}.${method.name} subscribes; it takes no arguments" }`,
2827
+ " reader.endArray()",
2828
+ " reader.endDocument()",
2347
2829
  ` ${qualified(module.name)}.${method.name}(${closure})`,
2348
2830
  " \"null\"",
2349
2831
  " }"
2350
2832
  ];
2351
2833
  }
2352
- var lines = [` ${JSON.stringify(method.name)} -> {`, ` require(args.length() == ${method.parameters.length}) { "${sourceId}.${module.name}.${method.name} expects ${method.parameters.length} arguments" }`];
2834
+ var lines = [` ${JSON.stringify(method.name)} -> {`];
2835
+ var arityMessage = `${sourceId}.${module.name}.${method.name} expects ${method.parameters.length} arguments`;
2353
2836
  method.parameters.forEach(function(parameter, index) {
2354
- lines.push(` val arg${index} = ${value(parameter.nativeType, `args.get(${index})`, JSON.stringify(`args[${index}]`))}`);
2837
+ lines.push(` val arg${index} = ${value(parameter.nativeType, "reader", JSON.stringify(`args[${index}]`))}`);
2838
+ if (index < method.parameters.length - 1) {
2839
+ lines.push(` require(reader.hasNext()) { ${JSON.stringify(arityMessage)} }`);
2840
+ }
2355
2841
  });
2842
+ lines.push(` require(!reader.hasNext()) { ${JSON.stringify(arityMessage)} }`);
2843
+ lines.push(" reader.endArray()", " reader.endDocument()");
2356
2844
  var call = `${qualified(module.name)}.${method.name}(${method.parameters.map(function(_, index) {
2357
2845
  return `arg${index}`;
2358
2846
  }).join(", ")})`;
@@ -2376,27 +2864,223 @@ function renderKotlinSourceGlue(sourceId, contract) {
2376
2864
  " }",
2377
2865
  "}",
2378
2866
  "",
2379
- ...contract.views.length === 0 ? [] : viewGlue(sourceId, contract, hash, value, encode)
2867
+ ...contract.views.length === 0 ? [] : viewGlue(sourceId, contract, hash, value, encode, qualifiedType)
2380
2868
  ].join("\n");
2381
2869
  return {
2382
2870
  hash,
2383
2871
  source
2384
2872
  };
2385
2873
  }
2386
- function viewGlue(sourceId, contract, hash, value, encode) {
2874
+ function viewGlue(sourceId, contract, hash, value, encode, qualifiedType) {
2387
2875
  var qualifier = contract.packageName ? `${contract.packageName}.` : "";
2876
+ var cap = function cap2(name) {
2877
+ return name.slice(0, 1).toUpperCase() + name.slice(1);
2878
+ };
2879
+ var readerClass = `OneJsonReader_${sourceId}`;
2880
+ var tokenClass = `OneJsonToken_${sourceId}`;
2881
+ var SEQ_KEY = "\"\\$seq\"";
2882
+ var BASE_KEY = "\"\\$base\"";
2883
+ var LISTS_KEY = "\"\\$lists\"";
2884
+ var listElement = function listElement2(type) {
2885
+ var inner = type.endsWith("?") ? type.slice(0, -1) : type;
2886
+ return inner.startsWith("List<") && inner.endsWith(">") ? inner.slice(5, -1) : null;
2887
+ };
2888
+ var holders = contract.views.flatMap(function(view) {
2889
+ return [
2890
+ `private class Held${view.name}_${sourceId} {`,
2891
+ ...view.props.flatMap(function(prop) {
2892
+ if (prop.callback) {
2893
+ return prop.nativeType.endsWith("?") ? [` var has${cap(prop.name)}: Boolean = false`] : [];
2894
+ }
2895
+ var declared = qualifiedType(prop.nativeType);
2896
+ if (prop.optional || prop.nativeType.endsWith("?")) return [` var ${prop.name}: ${declared} = null`];
2897
+ var zero = unboxedZero(prop.nativeType);
2898
+ if (zero !== null) return [` var ${prop.name}: ${declared} = ${zero}`];
2899
+ if (prop.nativeType === "String") return [` var ${prop.name}: String = ""`];
2900
+ return [` var ${prop.name}: ${declared}? = null`];
2901
+ }),
2902
+ "}"
2903
+ ];
2904
+ });
2905
+ var mergers = contract.views.flatMap(function(view) {
2906
+ return view.props.flatMap(function(prop) {
2907
+ if (prop.callback) return [];
2908
+ var elem = listElement(prop.nativeType);
2909
+ if (elem === null) return [];
2910
+ var element = qualifiedType(elem);
2911
+ return [
2912
+ `private fun mergeLists${view.name}${cap(prop.name)}(reader: ${readerClass}, old: List<${element}>?, prop: String): List<${element}>? {`,
2913
+ ` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("lists.$prop: expected object")`,
2914
+ " reader.beginObject()",
2915
+ " var length = -1",
2916
+ ` var set: List<Pair<Int, ${element}>>? = null`,
2917
+ " while (reader.hasNext()) {",
2918
+ " when (reader.selectName(\"length\", \"set\")) {",
2919
+ ` 0 -> length = readInt(reader, "lists.$prop.length")`,
2920
+ ` 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 }`,
2921
+ " else -> reader.skipValue()",
2922
+ " }",
2923
+ " }",
2924
+ " reader.endObject()",
2925
+ " if (length < 0) error(\"lists.$prop: needs length\")",
2926
+ " val current = old ?: return null",
2927
+ " val entries = set ?: emptyList()",
2928
+ " for (j in 1 until entries.size) require(entries[j].first > entries[j - 1].first) { \"lists.$prop: set out of order\" }",
2929
+ ` val fresh = ArrayList<${element}>(length)`,
2930
+ " var s = 0",
2931
+ " for (i in 0 until length) {",
2932
+ " if (s < entries.size && entries[s].first == i) { fresh.add(entries[s].second); s++ }",
2933
+ " else if (i < current.size) fresh.add(current[i])",
2934
+ " else return null",
2935
+ " }",
2936
+ " return fresh",
2937
+ "}"
2938
+ ];
2939
+ });
2940
+ });
2388
2941
  return [
2942
+ ...holders,
2943
+ ...mergers,
2944
+ "",
2389
2945
  `@Keep class OneNativeSourceViews_${sourceId}: OneNativeSourceViewDispatch {`,
2390
2946
  ` override val contractHash = ${JSON.stringify(hash)}`,
2947
+ " override fun apply(view: String, propsJson: String, state: OneNativeSourceKeyedState): Boolean {",
2948
+ ` val reader = ${readerClass}(propsJson)`,
2949
+ ` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("props must be a JSON object")`,
2950
+ " reader.beginObject()",
2951
+ " when (view) {",
2952
+ ...contract.views.flatMap(function(view) {
2953
+ var holder = `Held${view.name}_${sourceId}`;
2954
+ var lists = view.props.filter(function(prop) {
2955
+ return !prop.callback && listElement(prop.nativeType) !== null;
2956
+ });
2957
+ var options = [
2958
+ SEQ_KEY,
2959
+ BASE_KEY,
2960
+ LISTS_KEY,
2961
+ ...view.props.map(function(prop) {
2962
+ return JSON.stringify(prop.name);
2963
+ })
2964
+ ].join(", ");
2965
+ return [
2966
+ ` ${JSON.stringify(view.name)} -> {`,
2967
+ ` var held = state.views[${JSON.stringify(view.name)}] as? ${holder}`,
2968
+ " var seq = -1",
2969
+ " var hasSeq = false",
2970
+ " var base = -1",
2971
+ " var hasBase = false",
2972
+ " var sawLists = false",
2973
+ ...view.props.flatMap(function(prop) {
2974
+ if (prop.callback) {
2975
+ return prop.nativeType.endsWith("?") ? [` var ${prop.name}Doc: Boolean? = null`] : [];
2976
+ }
2977
+ var declared = qualifiedType(prop.nativeType);
2978
+ if (prop.optional || prop.nativeType.endsWith("?")) {
2979
+ return [` var ${prop.name}: ${declared} = null`, ` var has${cap(prop.name)} = false`];
2980
+ }
2981
+ var zero = unboxedZero(prop.nativeType);
2982
+ if (zero !== null) return [` var ${prop.name} = ${zero}`, ` var has${cap(prop.name)} = false`];
2983
+ return [` var ${prop.name}: ${declared}? = null`];
2984
+ }),
2985
+ ...lists.map(function(prop) {
2986
+ return ` var ${prop.name}MergeFailed = false`;
2987
+ }),
2988
+ " while (reader.hasNext()) {",
2989
+ ` when (reader.selectName(${options})) {`,
2990
+ " 0 -> { seq = readInt(reader, \"seq\"); hasSeq = true }",
2991
+ " 1 -> { base = readInt(reader, \"base\"); hasBase = true }",
2992
+ " 2 -> {",
2993
+ " sawLists = true",
2994
+ ` if (reader.peek() != ${tokenClass}.BEGIN_OBJECT) error("lists: expected object")`,
2995
+ " reader.beginObject()",
2996
+ " while (reader.hasNext()) {",
2997
+ ` when (reader.selectName(${lists.map(function(prop) {
2998
+ return JSON.stringify(prop.name);
2999
+ }).join(", ")})) {`,
3000
+ ...lists.map(function(prop, listsIndex) {
3001
+ return ` ${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)} }`;
3002
+ }),
3003
+ " else -> reader.skipValue()",
3004
+ " }",
3005
+ " }",
3006
+ " reader.endObject()",
3007
+ " }",
3008
+ ...view.props.map(function(prop, index) {
3009
+ var arm = index + 3;
3010
+ if (prop.callback) {
3011
+ return prop.nativeType.endsWith("?") ? ` ${arm} -> ${prop.name}Doc = readOptBoolean(reader)` : ` ${arm} -> reader.skipValue()`;
3012
+ }
3013
+ var path = JSON.stringify(`props.${prop.name}`);
3014
+ var read = `${prop.name} = ${value(prop.nativeType, "reader", path)}`;
3015
+ var needsFlag = prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null;
3016
+ return needsFlag ? ` ${arm} -> { ${read}; has${cap(prop.name)} = true }` : ` ${arm} -> ${read}`;
3017
+ }),
3018
+ " else -> reader.skipValue()",
3019
+ " }",
3020
+ " }",
3021
+ " reader.endObject()",
3022
+ " reader.endDocument()",
3023
+ " if (hasBase && !hasSeq) error(\"seq: required\")",
3024
+ " if (sawLists && !hasBase) error(\"lists: needs a change document\")",
3025
+ " if (!hasBase) {",
3026
+ ...view.props.flatMap(function(prop) {
3027
+ if (prop.callback || prop.optional) return [];
3028
+ if (prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
3029
+ return [` if (!has${cap(prop.name)}) error("props.${prop.name}: required")`];
3030
+ }
3031
+ return [];
3032
+ }),
3033
+ ` val h = held ?: ${holder}().also { state.views[${JSON.stringify(view.name)}] = it }`,
3034
+ ...view.props.flatMap(function(prop) {
3035
+ if (prop.callback) {
3036
+ return prop.nativeType.endsWith("?") ? [` h.has${cap(prop.name)} = ${prop.name}Doc == true`] : [];
3037
+ }
3038
+ if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
3039
+ return [` h.${prop.name} = ${prop.name}`];
3040
+ }
3041
+ return [` h.${prop.name} = (${prop.name} ?: error("props.${prop.name}: required"))`];
3042
+ }),
3043
+ " if (hasSeq) state.seq = seq",
3044
+ " state.revision++",
3045
+ " return true",
3046
+ " }",
3047
+ " if (seq == state.seq) return true",
3048
+ " val h = held",
3049
+ " if (base == state.seq && h != null) {",
3050
+ ...view.props.flatMap(function(prop) {
3051
+ if (prop.callback) {
3052
+ return prop.nativeType.endsWith("?") ? [` if (${prop.name}Doc != null) h.has${cap(prop.name)} = ${prop.name}Doc`] : [];
3053
+ }
3054
+ var mergeFailed = listElement(prop.nativeType) !== null ? [` if (${prop.name}MergeFailed) error("lists.${prop.name}: cannot merge")`] : [];
3055
+ if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null) {
3056
+ return [...mergeFailed, ` if (has${cap(prop.name)}) h.${prop.name} = ${prop.name}`];
3057
+ }
3058
+ return [...mergeFailed, ` if (${prop.name} != null) h.${prop.name} = ${prop.name}`];
3059
+ }),
3060
+ " state.seq = seq",
3061
+ " state.revision++",
3062
+ " return true",
3063
+ " }",
3064
+ " return false",
3065
+ " }"
3066
+ ];
3067
+ }),
3068
+ " else -> error(\"composable $view is not linked; rebuild the app\")",
3069
+ " }",
3070
+ " }",
2391
3071
  " @Composable",
2392
- " override fun Content(view: String, propsJson: String, emit: (String, String) -> Unit) {",
2393
- " val props = JSONObject(propsJson)",
3072
+ " override fun Content(view: String, state: OneNativeSourceKeyedState, emit: (String, String) -> Unit) {",
2394
3073
  " when (view) {",
2395
3074
  ...contract.views.flatMap(function(view) {
3075
+ var holder = `Held${view.name}_${sourceId}`;
2396
3076
  return [
2397
- ` ${JSON.stringify(view.name)} -> ${qualifier}${view.name}(`,
3077
+ ` ${JSON.stringify(view.name)} -> {`,
3078
+ " // the revision read resubscribes this scope on every apply",
3079
+ " val revision = state.revision",
3080
+ ` val held = state.views[${JSON.stringify(view.name)}] as? ${holder}`,
3081
+ " held?.let { h ->",
3082
+ ` ${qualifier}${view.name}(`,
2398
3083
  ...view.props.map(function(prop) {
2399
- var path = JSON.stringify(`props.${prop.name}`);
2400
3084
  if (prop.callback) {
2401
3085
  var args = prop.callback.map(function(_, index) {
2402
3086
  return `a${index}`;
@@ -2408,12 +3092,16 @@ function viewGlue(sourceId, contract, hash, value, encode) {
2408
3092
  return `.put(${encode(types[index], arg)})`;
2409
3093
  }).join("")}.toString())`;
2410
3094
  var lambda = `{ ${args.length > 0 ? `${args.join(", ")} -> ` : ""}${body} }`;
2411
- return ` ${prop.name} = ${prop.nativeType.endsWith("?") ? `if (props.optBoolean(${JSON.stringify(prop.name)})) (${lambda}) else null` : lambda},`;
3095
+ return ` ${prop.name} = ${prop.nativeType.endsWith("?") ? `if (h.has${cap(prop.name)}) (${lambda}) else null` : lambda},`;
2412
3096
  }
2413
- var input = prop.optional ? `(props.opt(${JSON.stringify(prop.name)}) ?: JSONObject.NULL)` : `(props.opt(${JSON.stringify(prop.name)}) ?: error(${JSON.stringify(`props.${prop.name}: required`)}))`;
2414
- return ` ${prop.name} = ${value(prop.nativeType, input, path)},`;
3097
+ if (prop.optional || prop.nativeType.endsWith("?") || unboxedZero(prop.nativeType) !== null || prop.nativeType === "String") {
3098
+ return ` ${prop.name} = h.${prop.name},`;
3099
+ }
3100
+ return ` ${prop.name} = h.${prop.name}!!,`;
2415
3101
  }),
2416
- " )"
3102
+ " )",
3103
+ " }",
3104
+ " }"
2417
3105
  ];
2418
3106
  }),
2419
3107
  " else -> error(\"composable $view is not linked; rebuild the app\")",