@hot-updater/react-native 1.0.8 → 1.1.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.
Files changed (145) hide show
  1. package/HotUpdater.podspec +14 -2
  2. package/Package.swift +24 -3
  3. package/android/build.gradle +2 -6
  4. package/android/hot-updater-fingerprint.gradle +135 -0
  5. package/android/src/main/cpp/HotUpdaterRecovery.cpp +291 -21
  6. package/android/src/main/java/com/hotupdater/ActivityStartGate.kt +176 -15
  7. package/android/src/main/java/com/hotupdater/ArtifactFileResolver.kt +20 -8
  8. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +145 -31
  9. package/android/src/main/java/com/hotupdater/CohortService.kt +10 -2
  10. package/android/src/main/java/com/hotupdater/FileManagerService.kt +3 -1
  11. package/android/src/main/java/com/hotupdater/HotUpdaterConfig.kt +249 -21
  12. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +8 -4
  13. package/android/src/main/java/com/hotupdater/HotUpdaterHeldContent.kt +92 -0
  14. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +319 -113
  15. package/android/src/main/java/com/hotupdater/HotUpdaterInstance.kt +87 -0
  16. package/android/src/main/java/com/hotupdater/HotUpdaterInstanceEnvironment.kt +323 -0
  17. package/android/src/main/java/com/hotupdater/HotUpdaterMainThread.kt +46 -0
  18. package/android/src/main/java/com/hotupdater/HotUpdaterProcess.kt +30 -0
  19. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryAttribution.kt +492 -0
  20. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +1541 -159
  21. package/android/src/main/java/com/hotupdater/HotUpdaterRecoverySlots.kt +137 -0
  22. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +4 -0
  23. package/android/src/main/java/com/hotupdater/HotUpdaterRuntimeInstance.kt +53 -0
  24. package/android/src/main/java/com/hotupdater/HotUpdaterUpdateCheck.kt +149 -0
  25. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +12 -2
  26. package/android/src/main/java/com/hotupdater/NativeConfigUtils.kt +5 -6
  27. package/android/src/main/java/com/hotupdater/NativeUpdateCheck.kt +172 -0
  28. package/android/src/main/java/com/hotupdater/NativeUpdateChecker.kt +1007 -0
  29. package/android/src/main/java/com/hotupdater/NativeUpdateEventQueue.kt +111 -0
  30. package/android/src/main/java/com/hotupdater/OkHttpUpdateCheckTransport.kt +88 -0
  31. package/android/src/main/java/com/hotupdater/ReleaseCatalog.kt +491 -0
  32. package/android/src/main/java/com/hotupdater/ReleaseCatalogClient.kt +405 -0
  33. package/android/src/main/java/com/hotupdater/ReleaseSelection.kt +474 -0
  34. package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +32 -10
  35. package/android/src/main/java/com/hotupdater/StrictJson.kt +392 -0
  36. package/android/src/main/java/com/hotupdater/VersionedPreferencesService.kt +24 -9
  37. package/android/src/main/java/com/hotupdater/WeakIdentitySet.kt +76 -0
  38. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BitReader.java +314 -0
  39. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliError.java +47 -0
  40. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliInputStream.java +174 -0
  41. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/BrotliRuntimeException.java +21 -0
  42. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Context.java +58 -0
  43. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decode.java +1574 -0
  44. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Decoder.java +72 -0
  45. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Dictionary.java +103 -0
  46. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/DictionaryData.java +82 -0
  47. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Huffman.java +143 -0
  48. package/{LICENSE → android/src/main/java/com/hotupdater/vendor/brotli/dec/LICENSE} +6 -20
  49. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/README.md +39 -0
  50. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/State.java +100 -0
  51. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Transform.java +240 -0
  52. package/android/src/main/java/com/hotupdater/vendor/brotli/dec/Utils.java +163 -0
  53. package/android/src/main/resources/META-INF/LICENSE-brotli +19 -0
  54. package/android/src/newarch/HotUpdater.kt +53 -15
  55. package/android/src/newarch/HotUpdaterModule.kt +42 -17
  56. package/android/src/newarch/JavaScriptExceptionHooks.kt +45 -8
  57. package/android/src/oldarch/HotUpdater.kt +51 -12
  58. package/android/src/oldarch/HotUpdaterModule.kt +43 -17
  59. package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
  60. package/android/src/oldarch/JavaScriptExceptionHooks.kt +25 -16
  61. package/ios/HotUpdater/Interface/HotUpdaterInterface.h +13 -0
  62. package/ios/HotUpdater/Internal/ArtifactFileResolver.swift +38 -7
  63. package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +10 -4
  64. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +186 -48
  65. package/ios/HotUpdater/Internal/CohortService.swift +8 -0
  66. package/ios/HotUpdater/Internal/HotUpdater.mm +821 -71
  67. package/ios/HotUpdater/Internal/HotUpdaterAsync.swift +7 -0
  68. package/ios/HotUpdater/Internal/HotUpdaterConfig.swift +208 -25
  69. package/ios/HotUpdater/Internal/HotUpdaterFingerprintHash.m +10 -0
  70. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +940 -261
  71. package/ios/HotUpdater/Internal/HotUpdaterInstanceEnvironment.swift +492 -0
  72. package/ios/HotUpdater/Internal/HotUpdaterInstanceRouter.swift +83 -0
  73. package/ios/HotUpdater/Internal/HotUpdaterInterface.swift +28 -1
  74. package/ios/HotUpdater/Internal/HotUpdaterLaunchRecovery.swift +1553 -0
  75. package/ios/HotUpdater/Internal/HotUpdaterNativeUpdates.swift +353 -0
  76. package/ios/HotUpdater/Internal/LaunchPreparationLock.swift +54 -0
  77. package/ios/HotUpdater/Internal/NativeUpdateCheck.swift +2051 -0
  78. package/ios/HotUpdater/Internal/ReleaseCatalog.swift +634 -0
  79. package/ios/HotUpdater/Internal/ReleaseCatalogCacheService.swift +1 -1
  80. package/ios/HotUpdater/Internal/ReleaseSelection.swift +482 -0
  81. package/ios/HotUpdater/Internal/SignatureVerifier.swift +27 -10
  82. package/ios/HotUpdater/Internal/StrictJson.swift +363 -0
  83. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +4 -2
  84. package/ios/HotUpdater/Internal/UpdateFailure.swift +2 -0
  85. package/ios/HotUpdater/Package.swift +11 -0
  86. package/ios/HotUpdater/Public/HotUpdater/HotUpdater.h +4 -0
  87. package/ios/HotUpdater/Public/HotUpdater.h +21 -2
  88. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +66 -0
  89. package/ios/Plugins/HotUpdaterFingerprintPlugin/HotUpdaterFingerprintPlugin.swift +45 -0
  90. package/ios/scripts/generate-fingerprint-hash.zsh +85 -0
  91. package/lib/commonjs/checkForUpdate.js +3 -1
  92. package/lib/commonjs/checkForUpdate.js.map +1 -1
  93. package/lib/commonjs/index.js +9 -12
  94. package/lib/commonjs/index.js.map +1 -1
  95. package/lib/commonjs/native.js +24 -79
  96. package/lib/commonjs/native.js.map +1 -1
  97. package/lib/commonjs/nativeUpdateEvents.js +66 -0
  98. package/lib/commonjs/nativeUpdateEvents.js.map +1 -0
  99. package/lib/commonjs/sdkVersion.js +1 -1
  100. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  101. package/lib/module/checkForUpdate.js +3 -1
  102. package/lib/module/checkForUpdate.js.map +1 -1
  103. package/lib/module/index.js +10 -13
  104. package/lib/module/index.js.map +1 -1
  105. package/lib/module/native.js +19 -73
  106. package/lib/module/native.js.map +1 -1
  107. package/lib/module/nativeUpdateEvents.js +61 -0
  108. package/lib/module/nativeUpdateEvents.js.map +1 -0
  109. package/lib/module/sdkVersion.js +1 -1
  110. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  111. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/index.d.ts +4 -14
  113. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/native.d.ts +9 -46
  115. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts +12 -0
  117. package/lib/typescript/commonjs/nativeUpdateEvents.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  119. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +19 -3
  120. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  121. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  122. package/lib/typescript/module/index.d.ts +4 -14
  123. package/lib/typescript/module/index.d.ts.map +1 -1
  124. package/lib/typescript/module/native.d.ts +9 -46
  125. package/lib/typescript/module/native.d.ts.map +1 -1
  126. package/lib/typescript/module/nativeUpdateEvents.d.ts +12 -0
  127. package/lib/typescript/module/nativeUpdateEvents.d.ts.map +1 -0
  128. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  129. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +19 -3
  130. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  131. package/package.json +4 -4
  132. package/src/checkForUpdate.ts +7 -1
  133. package/src/index.ts +9 -17
  134. package/src/native.ts +19 -91
  135. package/src/nativeUpdateEvents.ts +77 -0
  136. package/src/sdkVersion.ts +1 -1
  137. package/src/specs/NativeHotUpdater.ts +20 -3
  138. package/android/libs/README.md +0 -50
  139. package/android/libs/hot-updater-brotli-dec-1.2.0.jar +0 -0
  140. package/android/src/bridge/BridgeReactInstance.kt +0 -38
  141. package/android/src/bridgeless/BridgeReactInstance.kt +0 -24
  142. package/android/src/main/java/com/hotupdater/ContentAppearedLog.kt +0 -31
  143. package/android/src/main/java/com/hotupdater/ReactIntegrationManagerBase.kt +0 -34
  144. package/android/src/newarch/ReactIntegrationManager.kt +0 -212
  145. package/android/src/oldarch/ReactIntegrationManager.kt +0 -120
@@ -0,0 +1,363 @@
1
+ import Foundation
2
+
3
+ /// Parses exactly one JSON value (RFC 8259) as `JSON.parse` does, into
4
+ /// Foundation values: `[String: Any]`, `[Any]`, `String`, `NSNumber` (a
5
+ /// `Double`, or a Boolean) and `NSNull`. `JSONSerialization` accepts some
6
+ /// non-JSON syntax, such as a trailing comma in an object, and rejects some
7
+ /// JSON, such as an unpaired `\ud800` escape or a number that overflows to
8
+ /// infinity, so update responses are parsed here instead.
9
+ ///
10
+ /// Like `JSON.parse`, a duplicate key keeps its last value and an overflowing
11
+ /// number is infinite. Parsing is iterative, but releasing nested Swift arrays
12
+ /// and dictionaries recurses. So a value nested deeper than `maxDepth`, far
13
+ /// deeper than any field Hot Updater reads, is checked but not built: it
14
+ /// parses as `NSNull`, and the text is accepted as `JSON.parse` accepts it.
15
+ ///
16
+ /// JavaScript compares strings by UTF-16 code units, but Swift strings
17
+ /// compare canonically: `"scope\u{212A}ey"` (Kelvin sign) equals `"scopeKey"`,
18
+ /// and a Swift string cannot hold an unpaired surrogate at all.
19
+ /// - An object key that is not its own canonical decomposition, or that has
20
+ /// an unpaired surrogate, is stored escaped. Lookups by plain names match
21
+ /// only that exact key, and `originalKey` recovers the escaped one.
22
+ /// - A string value with an unpaired surrogate is an `UnpairedString`, its
23
+ /// code units as `JSON.parse` keeps them; every other value is a `String`
24
+ /// exactly as the JSON spells it.
25
+ enum StrictJson {
26
+ /// Starts every escaped key. A key that starts with it is escaped too.
27
+ private static let escapedKeyPrefix = "\u{E000}"
28
+
29
+ /// The deepest nesting of arrays and objects that is built.
30
+ static let maxDepth = 128
31
+
32
+ /// The object key under which `key` is stored.
33
+ static func storedKey(_ key: String) -> String {
34
+ storedKey(Array(key.utf16))
35
+ }
36
+
37
+ private static func storedKey(_ units: [UInt16]) -> String {
38
+ let key = String(decoding: units, as: UTF16.self)
39
+ if !needsEscape(units),
40
+ key.utf16.elementsEqual(key.decomposedStringWithCanonicalMapping.utf16) {
41
+ return key
42
+ }
43
+ return escaped(units)
44
+ }
45
+
46
+ /// A string value with an unpaired surrogate, which a Swift `String`
47
+ /// cannot hold: its UTF-16 code units, as `JSON.parse` keeps them.
48
+ struct UnpairedString: Hashable {
49
+ let units: [UInt16]
50
+
51
+ /// The text, each unpaired surrogate as U+FFFD.
52
+ var text: String { String(decoding: units, as: UTF16.self) }
53
+ }
54
+
55
+ /// The parsed value of a string with `units`.
56
+ private static func storedValue(_ units: [UInt16]) -> Any {
57
+ hasUnpairedSurrogate(units) ? UnpairedString(units: units) : String(decoding: units, as: UTF16.self)
58
+ }
59
+
60
+ /// Whether a Swift string would lose `units`, or they could be mistaken
61
+ /// for an escaped key.
62
+ private static func needsEscape(_ units: [UInt16]) -> Bool {
63
+ units.first == 0xE000 || hasUnpairedSurrogate(units)
64
+ }
65
+
66
+ private static func hasUnpairedSurrogate(_ units: [UInt16]) -> Bool {
67
+ var index = 0
68
+ while index < units.count {
69
+ let unit = units[index]
70
+ if (0xD800...0xDBFF).contains(unit) {
71
+ guard index + 1 < units.count, (0xDC00...0xDFFF).contains(units[index + 1]) else {
72
+ return true
73
+ }
74
+ index += 2
75
+ } else if (0xDC00...0xDFFF).contains(unit) {
76
+ return true
77
+ } else {
78
+ index += 1
79
+ }
80
+ }
81
+ return false
82
+ }
83
+
84
+ private static func escaped(_ units: [UInt16]) -> String {
85
+ escapedKeyPrefix + units.map { String(format: "%04x", $0) }.joined()
86
+ }
87
+
88
+ /// The key as the JSON text spelled it, for a key read from a parsed
89
+ /// object; an unpaired surrogate becomes U+FFFD.
90
+ static func originalKey(_ storedKey: String) -> String {
91
+ guard storedKey.hasPrefix(escapedKeyPrefix) else { return storedKey }
92
+ let hex = Array(storedKey.dropFirst(escapedKeyPrefix.count))
93
+ var units: [UInt16] = []
94
+ var index = 0
95
+ while index + 4 <= hex.count, let unit = UInt16(String(hex[index..<(index + 4)]), radix: 16) {
96
+ units.append(unit)
97
+ index += 4
98
+ }
99
+ return String(decoding: units, as: UTF16.self)
100
+ }
101
+
102
+ static func isValid(_ text: String) -> Bool {
103
+ parse(text) != nil
104
+ }
105
+
106
+ /// The value of `text`, or nil when `text` is not exactly one JSON value.
107
+ static func parse(_ text: String) -> Any? {
108
+ var parser = Parser(bytes: Array(text.utf8))
109
+ return parser.parse()
110
+ }
111
+
112
+ private final class Container {
113
+ let isObject: Bool
114
+ /// Past `maxDepth`: checked, but neither built nor kept.
115
+ let discards: Bool
116
+ var object: [String: Any] = [:]
117
+ var array: [Any] = []
118
+ var key = ""
119
+
120
+ init(isObject: Bool, depth: Int) {
121
+ self.isObject = isObject
122
+ discards = depth >= StrictJson.maxDepth
123
+ }
124
+
125
+ var value: Any {
126
+ if discards { return NSNull() }
127
+ return isObject ? object : array
128
+ }
129
+
130
+ func add(_ value: Any) {
131
+ if discards { return }
132
+ if isObject {
133
+ object[key] = value
134
+ } else {
135
+ array.append(value)
136
+ }
137
+ }
138
+ }
139
+
140
+ private struct Parser {
141
+ let bytes: [UInt8]
142
+ var index = 0
143
+ var containers: [Container] = []
144
+
145
+ init(bytes: [UInt8]) {
146
+ self.bytes = bytes
147
+ }
148
+
149
+ mutating func parse() -> Any? {
150
+ var expectValue = true
151
+ var result: Any?
152
+ // Adds a finished value to the open container, or ends the parse.
153
+ func finish(_ value: Any, _ containers: [Container]) -> Bool {
154
+ if let container = containers.last {
155
+ container.add(value)
156
+ return false
157
+ }
158
+ result = value
159
+ return true
160
+ }
161
+
162
+ while true {
163
+ skipWhitespace()
164
+ if expectValue {
165
+ let value: Any
166
+ switch peek() {
167
+ case UInt8(ascii: "{"):
168
+ index += 1
169
+ skipWhitespace()
170
+ if peek() == UInt8(ascii: "}") {
171
+ index += 1
172
+ value = [String: Any]()
173
+ } else {
174
+ let container = Container(isObject: true, depth: containers.count)
175
+ guard let key = member() else { return nil }
176
+ container.key = key
177
+ containers.append(container)
178
+ continue
179
+ }
180
+ case UInt8(ascii: "["):
181
+ index += 1
182
+ skipWhitespace()
183
+ if peek() == UInt8(ascii: "]") {
184
+ index += 1
185
+ value = [Any]()
186
+ } else {
187
+ containers.append(Container(isObject: false, depth: containers.count))
188
+ continue
189
+ }
190
+ case UInt8(ascii: "\""):
191
+ guard let units = string() else { return nil }
192
+ value = StrictJson.storedValue(units)
193
+ case UInt8(ascii: "t"):
194
+ guard literal("true") else { return nil }
195
+ value = NSNumber(value: true)
196
+ case UInt8(ascii: "f"):
197
+ guard literal("false") else { return nil }
198
+ value = NSNumber(value: false)
199
+ case UInt8(ascii: "n"):
200
+ guard literal("null") else { return nil }
201
+ value = NSNull()
202
+ default:
203
+ guard let number = number() else { return nil }
204
+ value = NSNumber(value: number)
205
+ }
206
+ expectValue = false
207
+ if finish(value, containers) {
208
+ skipWhitespace()
209
+ return index == bytes.count ? result : nil
210
+ }
211
+ } else {
212
+ guard let container = containers.last else { return nil }
213
+ switch peek() {
214
+ case UInt8(ascii: ","):
215
+ index += 1
216
+ if container.isObject {
217
+ skipWhitespace()
218
+ guard let key = member() else { return nil }
219
+ container.key = key
220
+ }
221
+ expectValue = true
222
+ case UInt8(ascii: "}") where container.isObject,
223
+ UInt8(ascii: "]") where !container.isObject:
224
+ index += 1
225
+ containers.removeLast()
226
+ if finish(container.value, containers) {
227
+ skipWhitespace()
228
+ return index == bytes.count ? result : nil
229
+ }
230
+ default:
231
+ return nil
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ /// An object member's stored key and colon; its value comes next.
238
+ private mutating func member() -> String? {
239
+ guard peek() == UInt8(ascii: "\""), let key = string() else { return nil }
240
+ skipWhitespace()
241
+ guard peek() == UInt8(ascii: ":") else { return nil }
242
+ index += 1
243
+ return StrictJson.storedKey(key)
244
+ }
245
+
246
+ /// A string's UTF-16 code units, as JSON.parse keeps them.
247
+ private mutating func string() -> [UInt16]? {
248
+ index += 1
249
+ var units: [UInt16] = []
250
+ var runStart = index
251
+ // Appends the unescaped UTF-8 bytes read since the last escape.
252
+ func flush(_ end: Int) {
253
+ if end > runStart {
254
+ units.append(contentsOf: String(decoding: bytes[runStart..<end], as: UTF8.self).utf16)
255
+ }
256
+ }
257
+ while index < bytes.count {
258
+ let byte = bytes[index]
259
+ if byte == UInt8(ascii: "\"") {
260
+ flush(index)
261
+ index += 1
262
+ return units
263
+ }
264
+ if byte < 0x20 { return nil }
265
+ if byte != UInt8(ascii: "\\") {
266
+ index += 1
267
+ continue
268
+ }
269
+ flush(index)
270
+ index += 1
271
+ guard index < bytes.count else { return nil }
272
+ let escape = bytes[index]
273
+ index += 1
274
+ switch escape {
275
+ case UInt8(ascii: "\""): units.append(0x22)
276
+ case UInt8(ascii: "\\"): units.append(0x5C)
277
+ case UInt8(ascii: "/"): units.append(0x2F)
278
+ case UInt8(ascii: "b"): units.append(0x08)
279
+ case UInt8(ascii: "f"): units.append(0x0C)
280
+ case UInt8(ascii: "n"): units.append(0x0A)
281
+ case UInt8(ascii: "r"): units.append(0x0D)
282
+ case UInt8(ascii: "t"): units.append(0x09)
283
+ case UInt8(ascii: "u"):
284
+ guard index + 4 <= bytes.count else { return nil }
285
+ var unit: UInt16 = 0
286
+ for _ in 0..<4 {
287
+ guard let digit = hexValue(bytes[index]) else { return nil }
288
+ unit = unit << 4 | UInt16(digit)
289
+ index += 1
290
+ }
291
+ units.append(unit)
292
+ default:
293
+ return nil
294
+ }
295
+ runStart = index
296
+ }
297
+ return nil
298
+ }
299
+
300
+ private mutating func number() -> Double? {
301
+ let start = index
302
+ if peek() == UInt8(ascii: "-") { index += 1 }
303
+ if peek() == UInt8(ascii: "0") {
304
+ index += 1
305
+ } else if let byte = peek(), (UInt8(ascii: "1")...UInt8(ascii: "9")).contains(byte) {
306
+ skipDigits()
307
+ } else {
308
+ return nil
309
+ }
310
+ if peek() == UInt8(ascii: ".") {
311
+ index += 1
312
+ guard isDigit(peek()) else { return nil }
313
+ skipDigits()
314
+ }
315
+ if peek() == UInt8(ascii: "e") || peek() == UInt8(ascii: "E") {
316
+ index += 1
317
+ if peek() == UInt8(ascii: "+") || peek() == UInt8(ascii: "-") { index += 1 }
318
+ guard isDigit(peek()) else { return nil }
319
+ skipDigits()
320
+ }
321
+ // An overflowing literal is infinite, as in JSON.parse.
322
+ return Double(String(decoding: bytes[start..<index], as: UTF8.self))
323
+ }
324
+
325
+ private mutating func literal(_ word: String) -> Bool {
326
+ let wordBytes = Array(word.utf8)
327
+ guard index + wordBytes.count <= bytes.count,
328
+ Array(bytes[index..<(index + wordBytes.count)]) == wordBytes else { return false }
329
+ index += wordBytes.count
330
+ return true
331
+ }
332
+
333
+ private mutating func skipWhitespace() {
334
+ while let byte = peek(),
335
+ byte == UInt8(ascii: " ") || byte == UInt8(ascii: "\t") ||
336
+ byte == UInt8(ascii: "\n") || byte == UInt8(ascii: "\r") {
337
+ index += 1
338
+ }
339
+ }
340
+
341
+ private mutating func skipDigits() {
342
+ while isDigit(peek()) { index += 1 }
343
+ }
344
+
345
+ private func peek() -> UInt8? {
346
+ index < bytes.count ? bytes[index] : nil
347
+ }
348
+
349
+ private func isDigit(_ byte: UInt8?) -> Bool {
350
+ guard let byte else { return false }
351
+ return (UInt8(ascii: "0")...UInt8(ascii: "9")).contains(byte)
352
+ }
353
+
354
+ private func hexValue(_ byte: UInt8) -> UInt8? {
355
+ switch byte {
356
+ case UInt8(ascii: "0")...UInt8(ascii: "9"): return byte - UInt8(ascii: "0")
357
+ case UInt8(ascii: "a")...UInt8(ascii: "f"): return byte - UInt8(ascii: "a") + 10
358
+ case UInt8(ascii: "A")...UInt8(ascii: "F"): return byte - UInt8(ascii: "A") + 10
359
+ default: return nil
360
+ }
361
+ }
362
+ }
363
+ }
@@ -162,7 +162,9 @@ class URLSessionDownloadService: NSObject, DownloadService {
162
162
  private var destinations: [URLSessionTask: String] = [:]
163
163
  private var fileSizeHandlers: [URLSessionTask: (Int64) -> Void] = [:]
164
164
 
165
- override init() {
165
+ /// Each instance passes its own `backgroundSessionIdentifier`: a process
166
+ /// must not create two background sessions with one identifier.
167
+ init(backgroundSessionIdentifier: String = HotUpdaterInstanceEnvironment.main.backgroundSessionIdentifier) {
166
168
  super.init()
167
169
 
168
170
  // Foreground session (existing behavior)
@@ -171,7 +173,7 @@ class URLSessionDownloadService: NSObject, DownloadService {
171
173
 
172
174
  // Background session for persistent downloads
173
175
  let backgroundConfig = URLSessionConfiguration.background(
174
- withIdentifier: "com.hotupdater.background.download"
176
+ withIdentifier: backgroundSessionIdentifier
175
177
  )
176
178
  backgroundConfig.isDiscretionary = false
177
179
  if #available(macOS 11.0, *) {
@@ -278,6 +278,8 @@ enum UpdateFailureClassifier {
278
278
  return classifySignatureError(error, during: phase)
279
279
  case let error as DownloadError:
280
280
  return classifyDownloadError(error)
281
+ case is ArtifactFileResolverError:
282
+ return .invalidResponse
281
283
  case let error as URLError:
282
284
  return classifyURLError(error)
283
285
  default:
@@ -12,9 +12,17 @@ let coreSources = [
12
12
  "FileManagerService.swift",
13
13
  "HashUtils.swift",
14
14
  "HotUpdaterConfig.swift",
15
+ "HotUpdaterInstanceEnvironment.swift",
16
+ "HotUpdaterInstanceRouter.swift",
17
+ "HotUpdaterLaunchRecovery.swift",
15
18
  "KeyValueStorageService.swift",
19
+ "LaunchPreparationLock.swift",
20
+ "NativeUpdateCheck.swift",
16
21
  "NotificationExtension.swift",
22
+ "ReleaseCatalog.swift",
17
23
  "ReleaseCatalogCacheService.swift",
24
+ "ReleaseSelection.swift",
25
+ "StrictJson.swift",
18
26
  "SignatureVerifier.swift",
19
27
  "UpdateFailure.swift",
20
28
  "URLSessionDownloadService.swift",
@@ -27,9 +35,12 @@ let coreExcludedFiles = [
27
35
  "CohortService.swift",
28
36
  "HotUpdater-Bridging-Header.h",
29
37
  "HotUpdater.mm",
38
+ "HotUpdaterAsync.swift",
30
39
  "HotUpdaterImpl.swift",
31
40
  "HotUpdaterInterface.swift",
32
41
  "HotUpdaterMinBundleId.m",
42
+ "HotUpdaterFingerprintHash.m",
43
+ "HotUpdaterNativeUpdates.swift",
33
44
  ]
34
45
 
35
46
  let bsdiffPatchBridgeExcludedFiles =
@@ -0,0 +1,4 @@
1
+ // Swift Package Manager only: the Objective-C++ target is HotUpdaterObjC there
2
+ // (see Package.swift), so this keeps `#import <HotUpdater/HotUpdater.h>`, the
3
+ // import CocoaPods builds use, working for Objective-C apps.
4
+ #import "../HotUpdater.h"
@@ -19,7 +19,9 @@
19
19
  /**
20
20
  * Returns the currently active bundle URL with specific bundle from the default (static) instance.
21
21
  * Callable from Objective-C (e.g., AppDelegate).
22
- * This is implemented in HotUpdater.mm and calls the Swift static method.
22
+ * The bundle holds the embedded main.jsbundle, such as a brownfield XCFramework's
23
+ * bundle. Hot Updater reuses it for reloads and reads its HOT_UPDATER_* Info.plist
24
+ * keys before the main bundle's.
23
25
  */
24
26
  + (NSURL *)bundleURLWithBundle:(NSBundle *)bundle NS_SWIFT_NAME(bundleURL(bundle:));
25
27
 
@@ -36,13 +38,30 @@
36
38
  - (NSURL *)bundleURLWithBundle:(NSBundle *)bundle NS_SWIFT_NAME(bundleURL(bundle:));
37
39
 
38
40
  /**
41
+ * Deprecated: pass `channel`, `fingerprintHash`, and `publicKey` to the
42
+ * `HotUpdaterOptions` of the `main` `HotUpdaterInstance` instead. Still
43
+ * honored for main; named instances never read it.
44
+ *
39
45
  * Programmatic configuration for brownfield / XCFramework setups where
40
46
  * Info.plist (host) or string resources (Android) are not desirable.
47
+ * A nil argument means no override. Each call replaces all four values.
48
+ *
49
+ * `channel` is the app's fixed default channel, not a runtime switch: keep it
50
+ * the same from the first launch on. It is part of the storage isolation
51
+ * key, so changing it deletes the downloaded bundles. To check another
52
+ * channel at runtime, pass `channel` to `HotUpdaterInstance.checkForUpdate`,
53
+ * and undo that with its `resetChannel`.
54
+ *
55
+ * `isolationKey`, when set, replaces main's storage isolation key verbatim.
56
+ * It is no longer needed: under the fingerprint strategy the default key is
57
+ * the fingerprint, the build's MIN_BUNDLE_ID, and the channel, so a native
58
+ * release that ships the same artifact keeps its downloads.
41
59
  */
42
60
  + (void)configureWithFingerprintHash:(nullable NSString *)fingerprintHash
43
61
  publicKey:(nullable NSString *)publicKey
44
62
  channel:(nullable NSString *)channel
45
- isolationKey:(nullable NSString *)isolationKey;
63
+ isolationKey:(nullable NSString *)isolationKey
64
+ DEPRECATED_MSG_ATTRIBUTE("Pass channel, fingerprintHash, and publicKey to HotUpdaterOptions. Fingerprint downloads now survive native releases that ship the same artifact.");
46
65
 
47
66
  /**
48
67
  * 다운로드 진행 상황 업데이트 시간을 추적하는 속성
@@ -4,6 +4,9 @@
4
4
  #ifdef RCT_NEW_ARCH_ENABLED
5
5
  #import <React/RCTFabricSurface.h>
6
6
  #import <React/RCTSurfacePresenter.h>
7
+ #import <React/RCTSurfaceView.h>
8
+ #else
9
+ #import <React/RCTRootView.h>
7
10
  #endif
8
11
 
9
12
  void HotUpdaterInstallRecoveryHooks(void)
@@ -42,8 +45,71 @@ void HotUpdaterInstallRecoveryHooks(void)
42
45
  // No coordination lock crosses the original call: older RN versions
43
46
  // invoke the completion synchronously on this same thread.
44
47
  ((void (*)(id, SEL))originalStart)(surface, startSelector);
48
+ // A surface of this runtime started: crash recovery starts the
49
+ // launch of the instance the runtime belongs to once its view is
50
+ // shown (see the attachment hook below).
51
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"HotUpdaterSurfaceWillStart" object:surface];
45
52
  }
46
53
  }));
54
+
55
+ // A surface started for pre-rendering is shown only once its view moves
56
+ // into a window: reported as another start, which crash recovery checks
57
+ // for being shown. RCTSurfaceView doesn't override didMoveToWindow: add
58
+ // an override that calls UIView's, never exchanging UIView's own.
59
+ Class surfaceViewClass = RCTSurfaceView.class;
60
+ SEL moveSelector = @selector(didMoveToWindow);
61
+ Class surfaceViewSuperclass = class_getSuperclass(surfaceViewClass);
62
+ Method inheritedMove = class_getInstanceMethod(surfaceViewClass, moveSelector);
63
+ if (inheritedMove && surfaceViewSuperclass) {
64
+ IMP currentMove = class_getMethodImplementation(surfaceViewClass, moveSelector);
65
+ IMP ownMove = currentMove != class_getMethodImplementation(surfaceViewSuperclass, moveSelector) ? currentMove : NULL;
66
+ IMP moveReplacement = imp_implementationWithBlock(^(RCTSurfaceView *view) {
67
+ IMP next = ownMove ?: class_getMethodImplementation(surfaceViewSuperclass, moveSelector);
68
+ ((void (*)(id, SEL))next)(view, moveSelector);
69
+ id surface = view.surface;
70
+ if (view.window && surface) {
71
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"HotUpdaterSurfaceWillStart" object:surface];
72
+ }
73
+ });
74
+ if (ownMove) {
75
+ method_setImplementation(inheritedMove, moveReplacement);
76
+ } else {
77
+ class_addMethod(surfaceViewClass, moveSelector, moveReplacement, method_getTypeEncoding(inheritedMove));
78
+ }
79
+ }
80
+ });
81
+ #else
82
+ // Old architecture: a root view in a window shows its bridge's runtime,
83
+ // which starts the launch of the instance the bridge runs (attributed by
84
+ // its bundle URL). Repeats are harmless: a launch starts once.
85
+ static dispatch_once_t onceToken;
86
+ dispatch_once(&onceToken, ^{
87
+ #pragma clang diagnostic push
88
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
89
+ Class rootViewClass = RCTRootView.class;
90
+ #pragma clang diagnostic pop
91
+ SEL selector = @selector(didMoveToWindow);
92
+ Class superclass = class_getSuperclass(rootViewClass);
93
+ Method inherited = class_getInstanceMethod(rootViewClass, selector);
94
+ if (!inherited || !superclass) {
95
+ return;
96
+ }
97
+ // RCTRootView doesn't override didMoveToWindow today: add an override
98
+ // that calls UIView's, never exchanging UIView's own implementation.
99
+ IMP current = class_getMethodImplementation(rootViewClass, selector);
100
+ IMP own = current != class_getMethodImplementation(superclass, selector) ? current : NULL;
101
+ IMP replacement = imp_implementationWithBlock(^(UIView *view) {
102
+ IMP next = own ?: class_getMethodImplementation(superclass, selector);
103
+ ((void (*)(id, SEL))next)(view, selector);
104
+ if (view.window) {
105
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"HotUpdaterSurfaceWillStart" object:view];
106
+ }
107
+ });
108
+ if (own) {
109
+ method_setImplementation(inherited, replacement);
110
+ } else {
111
+ class_addMethod(rootViewClass, selector, replacement, method_getTypeEncoding(inherited));
112
+ }
47
113
  });
48
114
  #endif
49
115
  }
@@ -0,0 +1,45 @@
1
+ import Foundation
2
+ import PackagePlugin
3
+
4
+ // Runs ios/scripts/generate-fingerprint-hash.zsh before every build of the
5
+ // target, the SwiftPM counterpart of the podspec's script phase. The script
6
+ // writes a Swift file defining HotUpdaterBuildFingerprintHash(), which
7
+ // HotUpdater.mm calls.
8
+ @main
9
+ struct HotUpdaterFingerprintPlugin: BuildToolPlugin {
10
+ func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
11
+ let script = context.package.directoryURL
12
+ .appending(path: "ios/scripts/generate-fingerprint-hash.zsh")
13
+ let outputDirectory = context.pluginWorkDirectoryURL
14
+ .appending(path: "Fingerprint")
15
+ // React Native's autolinker references this package through
16
+ // <app>/ios/build/generated/autolinking/libs/HotUpdater, as
17
+ // Package.swift's relative paths assume.
18
+ let appRoot = context.package.directoryURL
19
+ .appending(path: "../../../../../..")
20
+ .standardized
21
+ // A prebuild command gets only this environment. PATH lets the
22
+ // script find node, as React Native's own script phases do, when
23
+ // ios/.xcode.env.local sets no NODE_BINARY.
24
+ var environment: [String: String] = [:]
25
+ for key in ["HOT_UPDATER_APP_ROOT", "CONFIGURATION", "NODE_BINARY", "PATH"] {
26
+ if let value = ProcessInfo.processInfo.environment[key] {
27
+ environment[key] = value
28
+ }
29
+ }
30
+ return [
31
+ .prebuildCommand(
32
+ displayName: "[HotUpdater] Generate fingerprint",
33
+ executable: URL(filePath: "/bin/zsh"),
34
+ arguments: [
35
+ script.path(percentEncoded: false),
36
+ "--swift",
37
+ outputDirectory.path(percentEncoded: false),
38
+ appRoot.path(percentEncoded: false),
39
+ ],
40
+ environment: environment,
41
+ outputFilesDirectory: outputDirectory
42
+ ),
43
+ ]
44
+ }
45
+ }