@onekeyfe/react-native-split-bundle-loader 0.1.1 → 1.1.51

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.
@@ -9,6 +9,7 @@ import com.facebook.react.module.annotations.ReactModule
9
9
  import java.io.File
10
10
  import java.io.FileOutputStream
11
11
  import java.io.IOException
12
+ import java.util.concurrent.Semaphore
12
13
 
13
14
  /**
14
15
  * TurboModule entry point for SplitBundleLoader.
@@ -26,6 +27,9 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
26
27
  companion object {
27
28
  const val NAME = "SplitBundleLoader"
28
29
  private const val BUILTIN_EXTRACT_DIR = "onekey-builtin-segments"
30
+ // #18: Limit concurrent asset extractions to avoid I/O contention
31
+ private const val MAX_CONCURRENT_EXTRACTS = 2
32
+ private val extractSemaphore = Semaphore(MAX_CONCURRENT_EXTRACTS)
29
33
  }
30
34
 
31
35
  override fun getName(): String = NAME
@@ -73,11 +77,34 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
73
77
  result.putString("bundleVersion", bundleVersion)
74
78
 
75
79
  promise.resolve(result)
80
+
81
+ // #17: Clean up old version extract directories asynchronously
82
+ cleanupOldExtractDirs(context, nativeVersion)
76
83
  } catch (e: Exception) {
77
84
  promise.reject("SPLIT_BUNDLE_CONTEXT_ERROR", e.message, e)
78
85
  }
79
86
  }
80
87
 
88
+ // -----------------------------------------------------------------------
89
+ // resolveSegmentPath (Phase 3)
90
+ // -----------------------------------------------------------------------
91
+
92
+ override fun resolveSegmentPath(relativePath: String, sha256: String, promise: Promise) {
93
+ try {
94
+ val absolutePath = resolveSegmentPath(relativePath, sha256)
95
+ if (absolutePath != null) {
96
+ promise.resolve(absolutePath)
97
+ } else {
98
+ promise.reject(
99
+ "SPLIT_BUNDLE_NOT_FOUND",
100
+ "Segment file not found: $relativePath"
101
+ )
102
+ }
103
+ } catch (e: Exception) {
104
+ promise.reject("SPLIT_BUNDLE_RESOLVE_ERROR", e.message, e)
105
+ }
106
+ }
107
+
81
108
  // -----------------------------------------------------------------------
82
109
  // loadSegment
83
110
  // -----------------------------------------------------------------------
@@ -89,10 +116,14 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
89
116
  sha256: String,
90
117
  promise: Promise
91
118
  ) {
119
+ // NOTE (#44): sha256 param is not verified at load time by design.
120
+ // Per §6.4.1, runtime trusts that OTA install has already verified
121
+ // segment integrity. Builtin segments are signed as part of the APK/IPA.
122
+ // If runtime SHA-256 verification is needed, add it here.
92
123
  try {
93
124
  val segId = segmentId.toInt()
94
125
 
95
- val absolutePath = resolveSegmentPath(relativePath)
126
+ val absolutePath = resolveSegmentPath(relativePath, sha256)
96
127
  if (absolutePath == null) {
97
128
  promise.reject(
98
129
  "SPLIT_BUNDLE_NOT_FOUND",
@@ -101,49 +132,95 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
101
132
  return
102
133
  }
103
134
 
104
- // Register segment via CatalystInstance
135
+ // #19: Try CatalystInstance first (bridge mode), fall back to
136
+ // ReactHost registerSegment if available (bridgeless / new arch).
105
137
  val reactContext = reactApplicationContext
106
138
  if (reactContext.hasCatalystInstance()) {
107
139
  reactContext.catalystInstance.registerSegment(segId, absolutePath)
108
140
  SBLLogger.info("Loaded segment $segmentKey (id=$segId)")
109
141
  promise.resolve(null)
110
142
  } else {
111
- promise.reject(
112
- "SPLIT_BUNDLE_NO_INSTANCE",
113
- "CatalystInstance not available"
114
- )
143
+ // Bridgeless: try ReactHost via reflection
144
+ val registered = tryRegisterViaBridgeless(segId, absolutePath)
145
+ if (registered) {
146
+ SBLLogger.info("Loaded segment $segmentKey (id=$segId) via bridgeless")
147
+ promise.resolve(null)
148
+ } else {
149
+ promise.reject(
150
+ "SPLIT_BUNDLE_NO_INSTANCE",
151
+ "Neither CatalystInstance nor ReactHost available"
152
+ )
153
+ }
115
154
  }
116
155
  } catch (e: Exception) {
117
156
  promise.reject("SPLIT_BUNDLE_LOAD_ERROR", e.message, e)
118
157
  }
119
158
  }
120
159
 
160
+ // -----------------------------------------------------------------------
161
+ // Bridgeless support (#19)
162
+ // -----------------------------------------------------------------------
163
+
164
+ private fun tryRegisterViaBridgeless(segmentId: Int, path: String): Boolean {
165
+ return try {
166
+ val appContext = reactApplicationContext.applicationContext
167
+ val appClass = appContext.javaClass
168
+ val hostMethod = appClass.getMethod("getReactHost")
169
+ val host = hostMethod.invoke(appContext) ?: return false
170
+ val registerMethod = host.javaClass.getMethod(
171
+ "registerSegment", Int::class.java, String::class.java
172
+ )
173
+ registerMethod.invoke(host, segmentId, path)
174
+ true
175
+ } catch (_: Exception) {
176
+ false
177
+ }
178
+ }
179
+
121
180
  // -----------------------------------------------------------------------
122
181
  // Path resolution helpers
123
182
  // -----------------------------------------------------------------------
124
183
 
125
- private fun resolveSegmentPath(relativePath: String): String? {
184
+ /**
185
+ * Verify resolved path stays within the expected root directory (#45).
186
+ * Prevents path traversal via ".." components in relativePath.
187
+ */
188
+ private fun isPathWithinRoot(root: File, resolved: File): Boolean {
189
+ return resolved.canonicalPath.startsWith(root.canonicalPath + File.separator) ||
190
+ resolved.canonicalPath == root.canonicalPath
191
+ }
192
+
193
+ private fun resolveSegmentPath(relativePath: String, expectedSha256: String): String? {
194
+ // Path traversal guard (#45)
195
+ if (relativePath.contains("..")) {
196
+ SBLLogger.warn("Path traversal rejected: $relativePath")
197
+ return null
198
+ }
199
+
126
200
  // 1. Try OTA bundle directory first
127
201
  val otaBundlePath = getOtaBundlePath()
128
202
  if (!otaBundlePath.isNullOrEmpty()) {
129
203
  val otaRoot = File(otaBundlePath).parentFile
130
204
  if (otaRoot != null) {
131
205
  val candidate = File(otaRoot, relativePath)
132
- if (candidate.exists()) {
206
+ if (candidate.exists() && isPathWithinRoot(otaRoot, candidate)) {
133
207
  return candidate.absolutePath
134
208
  }
135
209
  }
136
210
  }
137
211
 
138
212
  // 2. Try builtin: extract from assets if needed
139
- return extractBuiltinSegmentIfNeeded(relativePath)
213
+ return extractBuiltinSegmentIfNeeded(relativePath, expectedSha256)
140
214
  }
141
215
 
142
216
  /**
143
217
  * For Android builtin segments, APK assets can't be passed directly as file paths.
144
218
  * Extract the asset to the extract cache directory on first access.
219
+ *
220
+ * #16: Validates extracted file size against the asset to detect truncated extractions.
221
+ * #18: Uses semaphore to limit concurrent extractions.
145
222
  */
146
- private fun extractBuiltinSegmentIfNeeded(relativePath: String): String? {
223
+ private fun extractBuiltinSegmentIfNeeded(relativePath: String, expectedSha256: String): String? {
147
224
  val context = reactApplicationContext
148
225
  val nativeVersion = try {
149
226
  context.packageManager
@@ -155,32 +232,101 @@ class SplitBundleLoaderModule(reactContext: ReactApplicationContext) :
155
232
  val extractDir = File(context.filesDir, "$BUILTIN_EXTRACT_DIR/$nativeVersion")
156
233
  val extractedFile = File(extractDir, relativePath)
157
234
 
158
- // Already extracted
235
+ // #16: If file exists, verify it's not truncated by checking size against asset
159
236
  if (extractedFile.exists()) {
160
- return extractedFile.absolutePath
237
+ val assetSize = getAssetSize(context.assets, relativePath)
238
+ if (assetSize >= 0 && extractedFile.length() == assetSize) {
239
+ return extractedFile.absolutePath
240
+ }
241
+ // Truncated or size mismatch — delete and re-extract
242
+ SBLLogger.warn("Extracted file size mismatch for $relativePath, re-extracting")
243
+ extractedFile.delete()
161
244
  }
162
245
 
163
- // Extract from assets
164
- val assets: AssetManager = context.assets
165
- return try {
166
- assets.open(relativePath).use { input ->
167
- extractedFile.parentFile?.let { parent ->
168
- if (!parent.exists()) parent.mkdirs()
246
+ // #18: Limit concurrent extractions
247
+ extractSemaphore.acquire()
248
+ try {
249
+ // Double-check after acquiring semaphore (another thread may have extracted)
250
+ if (extractedFile.exists()) {
251
+ return extractedFile.absolutePath
252
+ }
253
+
254
+ val assets: AssetManager = context.assets
255
+ return try {
256
+ // Extract to temp file first, then atomically rename
257
+ val tempFile = File(extractedFile.parentFile, "${extractedFile.name}.tmp")
258
+ assets.open(relativePath).use { input ->
259
+ extractedFile.parentFile?.let { parent ->
260
+ if (!parent.exists()) parent.mkdirs()
261
+ }
262
+ FileOutputStream(tempFile).use { output ->
263
+ val buffer = ByteArray(8192)
264
+ var len: Int
265
+ while (input.read(buffer).also { len = it } != -1) {
266
+ output.write(buffer, 0, len)
267
+ }
268
+ }
269
+ }
270
+ // Atomic rename prevents partial file observation
271
+ if (tempFile.renameTo(extractedFile)) {
272
+ extractedFile.absolutePath
273
+ } else {
274
+ tempFile.delete()
275
+ null
169
276
  }
170
- FileOutputStream(extractedFile).use { output ->
277
+ } catch (_: IOException) {
278
+ null
279
+ }
280
+ } finally {
281
+ extractSemaphore.release()
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Returns the size of an asset file, or -1 if it can't be determined.
287
+ */
288
+ private fun getAssetSize(assets: AssetManager, assetPath: String): Long {
289
+ return try {
290
+ assets.openFd(assetPath).use { it.length }
291
+ } catch (_: IOException) {
292
+ // Asset may be compressed; fall back to reading the stream
293
+ try {
294
+ assets.open(assetPath).use { input ->
295
+ var size = 0L
171
296
  val buffer = ByteArray(8192)
172
297
  var len: Int
173
298
  while (input.read(buffer).also { len = it } != -1) {
174
- output.write(buffer, 0, len)
299
+ size += len
175
300
  }
301
+ size
176
302
  }
303
+ } catch (_: IOException) {
304
+ -1
177
305
  }
178
- extractedFile.absolutePath
179
- } catch (_: IOException) {
180
- null
181
306
  }
182
307
  }
183
308
 
309
+ /**
310
+ * #17: Asynchronously clean up extract directories from previous native versions.
311
+ */
312
+ private fun cleanupOldExtractDirs(context: Context, currentVersion: String) {
313
+ Thread {
314
+ try {
315
+ val baseDir = File(context.filesDir, BUILTIN_EXTRACT_DIR)
316
+ if (!baseDir.exists() || !baseDir.isDirectory) return@Thread
317
+ val dirs = baseDir.listFiles() ?: return@Thread
318
+ for (dir in dirs) {
319
+ if (dir.isDirectory && dir.name != currentVersion) {
320
+ SBLLogger.info("Cleaning up old extract dir: ${dir.name}")
321
+ dir.deleteRecursively()
322
+ }
323
+ }
324
+ } catch (e: Exception) {
325
+ SBLLogger.warn("Failed to cleanup old extract dirs: ${e.message}")
326
+ }
327
+ }.start()
328
+ }
329
+
184
330
  private fun getOtaBundlePath(): String? {
185
331
  return try {
186
332
  val bundleUpdateStore = Class.forName(
@@ -10,5 +10,9 @@
10
10
  sha256:(NSString *)sha256
11
11
  resolve:(RCTPromiseResolveBlock)resolve
12
12
  reject:(RCTPromiseRejectBlock)reject;
13
+ - (void)resolveSegmentPath:(NSString *)relativePath
14
+ sha256:(NSString *)sha256
15
+ resolve:(RCTPromiseResolveBlock)resolve
16
+ reject:(RCTPromiseRejectBlock)reject;
13
17
 
14
18
  @end
@@ -2,6 +2,11 @@
2
2
  #import "SBLLogger.h"
3
3
  #import <React/RCTBridge.h>
4
4
 
5
+ // Bridgeless (New Architecture) support: RCTHost segment registration
6
+ @interface RCTHost (SplitBundle)
7
+ - (void)registerSegmentWithId:(NSNumber *)segmentId path:(NSString *)path;
8
+ @end
9
+
5
10
  @implementation SplitBundleLoader
6
11
 
7
12
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
@@ -21,6 +26,78 @@
21
26
  return NO;
22
27
  }
23
28
 
29
+ // MARK: - OTA bundle path helper
30
+
31
+ /// Safely retrieves the OTA bundle path via typed NSInvocation to avoid
32
+ /// performSelector ARC/signature issues (#15).
33
+ + (nullable NSString *)otaBundlePath
34
+ {
35
+ Class cls = NSClassFromString(@"ReactNativeBundleUpdate.BundleUpdateStore");
36
+ if (!cls) return nil;
37
+
38
+ SEL sel = NSSelectorFromString(@"currentBundleMainJSBundle");
39
+ if (![cls respondsToSelector:sel]) return nil;
40
+
41
+ NSMethodSignature *sig = [cls methodSignatureForSelector:sel];
42
+ if (!sig || strcmp(sig.methodReturnType, @encode(id)) != 0) {
43
+ [SBLLogger warn:@"OTA method signature mismatch — skipping"];
44
+ return nil;
45
+ }
46
+
47
+ NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
48
+ inv.target = cls;
49
+ inv.selector = sel;
50
+ [inv invoke];
51
+
52
+ __unsafe_unretained id rawResult = nil;
53
+ [inv getReturnValue:&rawResult];
54
+ if (![rawResult isKindOfClass:[NSString class]]) return nil;
55
+
56
+ NSString *result = (NSString *)rawResult;
57
+ if (result.length == 0) return nil;
58
+
59
+ if ([result hasPrefix:@"file://"]) {
60
+ result = [[NSURL URLWithString:result] path];
61
+ }
62
+ return result;
63
+ }
64
+
65
+ // MARK: - Segment registration helper
66
+
67
+ /// Registers a segment with the current runtime, supporting both legacy bridge
68
+ /// and bridgeless (RCTHost) architectures (#13).
69
+ ///
70
+ /// Thread safety (#57): This method is called from the TurboModule (JS thread).
71
+ /// RCTBridge.registerSegmentWithId:path: internally registers the segment with
72
+ /// the Hermes runtime on the JS thread, which is the correct calling context.
73
+ /// No queue dispatch is needed.
74
+ + (BOOL)registerSegment:(int)segmentId path:(NSString *)path error:(NSError **)outError
75
+ {
76
+ // Try legacy bridge first
77
+ RCTBridge *bridge = [RCTBridge currentBridge];
78
+ if (bridge && [bridge respondsToSelector:@selector(registerSegmentWithId:path:)]) {
79
+ [bridge registerSegmentWithId:@(segmentId) path:path];
80
+ return YES;
81
+ }
82
+
83
+ // Try bridgeless RCTHost via AppDelegate
84
+ id<UIApplicationDelegate> appDelegate = [UIApplication sharedApplication].delegate;
85
+ if ([appDelegate respondsToSelector:NSSelectorFromString(@"reactHost")]) {
86
+ id host = [appDelegate performSelector:NSSelectorFromString(@"reactHost")];
87
+ if (host && [host respondsToSelector:@selector(registerSegmentWithId:path:)]) {
88
+ [host registerSegmentWithId:@(segmentId) path:path];
89
+ return YES;
90
+ }
91
+ }
92
+
93
+ if (outError) {
94
+ *outError = [NSError errorWithDomain:@"SplitBundleLoader"
95
+ code:1
96
+ userInfo:@{NSLocalizedDescriptionKey: @"Neither RCTBridge nor RCTHost available for segment registration"}];
97
+ }
98
+ return NO;
99
+ }
100
+
24
101
  // MARK: - getRuntimeBundleContext
25
102
 
26
103
  - (void)getRuntimeBundleContext:(RCTPromiseResolveBlock)resolve
@@ -33,31 +110,12 @@
33
110
  NSString *nativeVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] ?: @"";
34
111
  NSString *bundleVersion = @"";
35
112
 
36
- // Check if OTA bundle is active via BundleUpdateStore
37
- Class bundleUpdateStoreClass = NSClassFromString(@"ReactNativeBundleUpdate.BundleUpdateStore");
38
- if (bundleUpdateStoreClass) {
39
- NSString *otaBundlePath = nil;
40
- SEL sel = NSSelectorFromString(@"currentBundleMainJSBundle");
41
- if ([bundleUpdateStoreClass respondsToSelector:sel]) {
42
- #pragma clang diagnostic push
43
- #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
44
- id result = [bundleUpdateStoreClass performSelector:sel];
45
- #pragma clang diagnostic pop
46
- otaBundlePath = [result isKindOfClass:[NSString class]] ? (NSString *)result : nil;
47
- }
48
- if (otaBundlePath && otaBundlePath.length > 0) {
49
- NSString *filePath = otaBundlePath;
50
- if ([otaBundlePath hasPrefix:@"file://"]) {
51
- filePath = [[NSURL URLWithString:otaBundlePath] path];
52
- }
53
- if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
54
- sourceKind = @"ota";
55
- bundleRoot = [filePath stringByDeletingLastPathComponent];
56
- }
57
- }
113
+ NSString *otaPath = [SplitBundleLoader otaBundlePath];
114
+ if (otaPath && [[NSFileManager defaultManager] fileExistsAtPath:otaPath]) {
115
+ sourceKind = @"ota";
116
+ bundleRoot = [otaPath stringByDeletingLastPathComponent];
58
117
  }
59
118
 
60
- // Builtin: use main bundle resource path
61
119
  if ([sourceKind isEqualToString:@"builtin"]) {
62
120
  bundleRoot = [[NSBundle mainBundle] resourcePath] ?: @"";
63
121
  }
@@ -74,6 +132,62 @@
74
132
  }
75
133
  }
76
134
 
135
+ // MARK: - Path resolution helper
136
+
137
+ /// Resolves a relative segment path to an absolute path, checking OTA then builtin.
138
+ /// Returns nil if the segment file is not found.
139
+ + (nullable NSString *)resolveAbsolutePath:(NSString *)relativePath
140
+ {
141
+ // 1. Try OTA bundle root first
142
+ NSString *otaPath = [SplitBundleLoader otaBundlePath];
143
+ if (otaPath) {
144
+ NSString *otaRoot = [otaPath stringByDeletingLastPathComponent];
145
+ NSString *candidate = [[otaRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath];
146
+ if ([candidate hasPrefix:otaRoot] &&
147
+ [[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
148
+ return candidate;
149
+ }
150
+ }
151
+
152
+ // 2. Fallback to builtin resource path
153
+ NSString *builtinRoot = [[NSBundle mainBundle] resourcePath];
154
+ NSString *candidate = [[builtinRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath];
155
+ if ([candidate hasPrefix:builtinRoot] &&
156
+ [[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
157
+ return candidate;
158
+ }
159
+
160
+ return nil;
161
+ }
162
+
163
+ // MARK: - resolveSegmentPath (Phase 3)
164
+
165
+ - (void)resolveSegmentPath:(NSString *)relativePath
166
+ sha256:(NSString *)sha256
167
+ resolve:(RCTPromiseResolveBlock)resolve
168
+ reject:(RCTPromiseRejectBlock)reject
169
+ {
170
+ @try {
171
+ if ([relativePath containsString:@".."]) {
172
+ reject(@"SPLIT_BUNDLE_INVALID_PATH",
173
+ [NSString stringWithFormat:@"Path traversal rejected: %@", relativePath],
174
+ nil);
175
+ return;
176
+ }
177
+
178
+ NSString *absolutePath = [SplitBundleLoader resolveAbsolutePath:relativePath];
179
+ if (absolutePath) {
180
+ resolve(absolutePath);
181
+ } else {
182
+ reject(@"SPLIT_BUNDLE_NOT_FOUND",
183
+ [NSString stringWithFormat:@"Segment file not found: %@", relativePath],
184
+ nil);
185
+ }
186
+ } @catch (NSException *exception) {
187
+ reject(@"SPLIT_BUNDLE_RESOLVE_ERROR", exception.reason, nil);
188
+ }
189
+ }
190
+
77
191
  // MARK: - loadSegment
78
192
 
79
193
  - (void)loadSegment:(double)segmentId
@@ -86,42 +200,15 @@
86
200
  @try {
87
201
  int segId = (int)segmentId;
88
202
 
89
- // Resolve absolute path
90
- NSString *absolutePath = nil;
91
-
92
- // 1. Try OTA bundle root first
93
- Class bundleUpdateStoreClass = NSClassFromString(@"ReactNativeBundleUpdate.BundleUpdateStore");
94
- if (bundleUpdateStoreClass) {
95
- NSString *otaBundlePath = nil;
96
- SEL sel = NSSelectorFromString(@"currentBundleMainJSBundle");
97
- if ([bundleUpdateStoreClass respondsToSelector:sel]) {
98
- #pragma clang diagnostic push
99
- #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
100
- id result = [bundleUpdateStoreClass performSelector:sel];
101
- #pragma clang diagnostic pop
102
- otaBundlePath = [result isKindOfClass:[NSString class]] ? (NSString *)result : nil;
103
- }
104
- if (otaBundlePath && otaBundlePath.length > 0) {
105
- NSString *filePath = otaBundlePath;
106
- if ([otaBundlePath hasPrefix:@"file://"]) {
107
- filePath = [[NSURL URLWithString:otaBundlePath] path];
108
- }
109
- NSString *otaRoot = [filePath stringByDeletingLastPathComponent];
110
- NSString *candidate = [otaRoot stringByAppendingPathComponent:relativePath];
111
- if ([[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
112
- absolutePath = candidate;
113
- }
114
- }
203
+ // Path traversal guard (#45)
204
+ if ([relativePath containsString:@".."]) {
205
+ reject(@"SPLIT_BUNDLE_INVALID_PATH",
206
+ [NSString stringWithFormat:@"Path traversal rejected: %@", relativePath],
207
+ nil);
208
+ return;
115
209
  }
116
210
 
117
- // 2. Fallback to builtin resource path
118
- if (!absolutePath) {
119
- NSString *builtinRoot = [[NSBundle mainBundle] resourcePath];
120
- NSString *candidate = [builtinRoot stringByAppendingPathComponent:relativePath];
121
- if ([[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
122
- absolutePath = candidate;
123
- }
124
- }
211
+ NSString *absolutePath = [SplitBundleLoader resolveAbsolutePath:relativePath];
125
212
 
126
213
  if (!absolutePath) {
127
214
  reject(@"SPLIT_BUNDLE_NOT_FOUND",
@@ -130,14 +217,15 @@
130
217
  return;
131
218
  }
132
219
 
133
- // Register segment via RCTBridge
134
- RCTBridge *bridge = [RCTBridge currentBridge];
135
- if (bridge) {
136
- [bridge registerSegmentWithId:@(segId) path:absolutePath];
220
+ // Register segment (#13: supports both bridge and bridgeless)
221
+ NSError *regError = nil;
222
+ if ([SplitBundleLoader registerSegment:segId path:absolutePath error:&regError]) {
137
223
  [SBLLogger info:[NSString stringWithFormat:@"Loaded segment %@ (id=%d)", segmentKey, segId]];
138
224
  resolve(nil);
139
225
  } else {
140
- reject(@"SPLIT_BUNDLE_NO_BRIDGE", @"RCTBridge not available", nil);
226
+ reject(@"SPLIT_BUNDLE_NO_RUNTIME",
227
+ regError.localizedDescription ?: @"Runtime not available",
228
+ regError);
141
229
  }
142
230
  } @catch (NSException *exception) {
143
231
  reject(@"SPLIT_BUNDLE_LOAD_ERROR",
@@ -1 +1 @@
1
- {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSplitBundleLoader.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAAQ,cAAc;AAqBlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,mBAAmB,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeSplitBundleLoader.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAAQ,cAAc;AAsBlD,eAAeA,mBAAmB,CAACC,YAAY,CAAO,mBAAmB,CAAC","ignoreList":[]}
@@ -9,6 +9,7 @@ export interface Spec extends TurboModule {
9
9
  bundleVersion?: string;
10
10
  }>;
11
11
  loadSegment(segmentId: number, segmentKey: string, relativePath: string, sha256: string): Promise<void>;
12
+ resolveSegmentPath(relativePath: string, sha256: string): Promise<string>;
12
13
  }
13
14
  declare const _default: Spec;
14
15
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"NativeSplitBundleLoader.d.ts","sourceRoot":"","sources":["../../../src/NativeSplitBundleLoader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,uBAAuB,IAAI,OAAO,CAAC;QACjC,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;IACH,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB;;AAED,wBAA2E"}
1
+ {"version":3,"file":"NativeSplitBundleLoader.d.ts","sourceRoot":"","sources":["../../../src/NativeSplitBundleLoader.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,uBAAuB,IAAI,OAAO,CAAC;QACjC,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,aAAa,EAAE,MAAM,CAAC;QACtB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC,CAAC;IACH,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3E;;AAED,wBAA2E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onekeyfe/react-native-split-bundle-loader",
3
- "version": "0.1.1",
3
+ "version": "1.1.51",
4
4
  "description": "react-native-split-bundle-loader",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -17,6 +17,7 @@ export interface Spec extends TurboModule {
17
17
  relativePath: string,
18
18
  sha256: string,
19
19
  ): Promise<void>;
20
+ resolveSegmentPath(relativePath: string, sha256: string): Promise<string>;
20
21
  }
21
22
 
22
23
  export default TurboModuleRegistry.getEnforcing<Spec>('SplitBundleLoader');