expo-tiddlywiki-filesystem-android-external-storage 2.6.0 → 2.8.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.
@@ -568,6 +568,10 @@ class ExternalStorageModule : Module() {
568
568
  GitHelper.gitPush(gitRootDir, remoteName, localBranch, remoteBranch, force, headers)
569
569
  }
570
570
 
571
+ AsyncFunction("gitCreateBundle") { gitRootDir: String, remoteName: String, localBranch: String, remoteBranch: String ->
572
+ GitHelper.gitCreateBundle(gitRootDir, remoteName, localBranch, remoteBranch)
573
+ }
574
+
571
575
  AsyncFunction("gitFetch") { gitRootDir: String, remoteName: String, branch: String, headers: String? ->
572
576
  GitHelper.gitFetch(gitRootDir, remoteName, branch, headers)
573
577
  }
@@ -84,17 +84,48 @@ internal object GitHelper {
84
84
  }
85
85
 
86
86
  /**
87
- * Ensure repository git config forces protocol.version=0.
87
+ * Ensure repository git config forces protocol.version=0 and
88
+ * constrains pack memory for Android's limited heap.
89
+ *
88
90
  * TidGi Desktop's git server uses `git --stateless-rpc` which only speaks V0/V1.
89
91
  * JGit defaults to V2 negotiation, which causes the error:
90
92
  * "Starting read stage without written request data pending is not supported"
91
93
  * because the server doesn't handle V2 capability advertisements.
94
+ *
95
+ * Pack memory limits prevent OOM on large repos (Android heap is ~268MB).
96
+ * Default JGit settings: deltaCacheSize=50MB, windowMemory=unlimited,
97
+ * bigFileThreshold=50MB — far too much for a mobile device.
92
98
  */
93
99
  private fun ensureProtocolV0(repo: Repository) {
94
100
  val config = repo.config
101
+ var dirty = false
95
102
  val current = config.getString("protocol", null, "version")
96
103
  if (current != "0") {
97
104
  config.setString("protocol", null, "version", "0")
105
+ dirty = true
106
+ }
107
+ // Limit pack memory to avoid OOM on push
108
+ // pack.windowMemory: max bytes for delta search window (per thread), default unlimited
109
+ if (config.getLong("pack", "windowmemory", 0) == 0L) {
110
+ config.setLong("pack", null, "windowmemory", 10L * 1024 * 1024) // 10MB
111
+ dirty = true
112
+ }
113
+ // pack.deltaCacheSize: total delta cache, default 50MB
114
+ if (config.getLong("pack", "deltacachesize", 50L * 1024 * 1024) >= 50L * 1024 * 1024) {
115
+ config.setLong("pack", null, "deltacachesize", 5L * 1024 * 1024) // 5MB
116
+ dirty = true
117
+ }
118
+ // pack.threads: limit to 1 to reduce memory pressure
119
+ if (config.getInt("pack", "threads", 0) == 0) {
120
+ config.setInt("pack", null, "threads", 1)
121
+ dirty = true
122
+ }
123
+ // pack.window: reduce from 10 to 5
124
+ if (config.getInt("pack", "window", 10) > 5) {
125
+ config.setInt("pack", null, "window", 5)
126
+ dirty = true
127
+ }
128
+ if (dirty) {
98
129
  config.save()
99
130
  }
100
131
  }
@@ -338,6 +369,80 @@ internal object GitHelper {
338
369
  return result.toString()
339
370
  }
340
371
 
372
+ // ─── Git bundle creation (for push via HTTP POST) ───────────────
373
+
374
+ /**
375
+ * Create a git bundle containing commits on localBranch that are ahead of
376
+ * remoteName/remoteBranch. Returns a base64-encoded bundle string, or
377
+ * a JSON error if there's nothing to bundle.
378
+ *
379
+ * This avoids JGit's broken SmartHttpPushConnection (which throws
380
+ * "Starting read stage without written request data pending is not supported"
381
+ * due to MultiRequestService not marking finalRequest=true for push).
382
+ *
383
+ * The bundle is sent by the JS layer via HTTP POST to the desktop's
384
+ * /receive-bundle endpoint, which runs `git fetch <bundle> master:mobile-incoming`.
385
+ */
386
+ fun gitCreateBundle(
387
+ gitRootDir: String,
388
+ remoteName: String,
389
+ localBranch: String,
390
+ remoteBranch: String
391
+ ): String {
392
+ val result = JSONObject()
393
+ try {
394
+ val repo = openRepo(gitRootDir)
395
+ try {
396
+ ensureProtocolV0(repo)
397
+ val localRef = repo.resolve("refs/heads/$localBranch")
398
+ ?: throw Exception("Local branch $localBranch not found")
399
+ val remoteRef = repo.resolve("refs/remotes/$remoteName/$remoteBranch")
400
+
401
+ val revWalk = RevWalk(repo)
402
+ try {
403
+ val localCommit = revWalk.parseCommit(localRef)
404
+
405
+ val bundleWriter = org.eclipse.jgit.transport.BundleWriter(repo)
406
+
407
+ // Include the local branch tip
408
+ bundleWriter.include("refs/heads/$localBranch", localRef)
409
+
410
+ // If we have a remote tracking ref, mark it as assumed (prerequisite).
411
+ // The receiving end must have this commit.
412
+ if (remoteRef != null) {
413
+ val remoteCommit = revWalk.parseCommit(remoteRef)
414
+ bundleWriter.assume(remoteCommit)
415
+ }
416
+
417
+ // Configure pack for low memory (Android)
418
+ val packConfig = org.eclipse.jgit.storage.pack.PackConfig(repo)
419
+ bundleWriter.setPackConfig(packConfig)
420
+
421
+ // Write bundle to memory
422
+ val baos = ByteArrayOutputStream()
423
+ bundleWriter.writeBundle(org.eclipse.jgit.lib.NullProgressMonitor.INSTANCE, baos)
424
+
425
+ val bundleBytes = baos.toByteArray()
426
+ val base64Bundle = Base64.encodeToString(bundleBytes, Base64.NO_WRAP)
427
+
428
+ result.put("ok", true)
429
+ result.put("bundle", base64Bundle)
430
+ result.put("bundleSize", bundleBytes.size)
431
+ android.util.Log.i("GitBundle", "Bundle created: ${bundleBytes.size} bytes, local=$localBranch remote=$remoteName/$remoteBranch")
432
+ } finally {
433
+ revWalk.close()
434
+ }
435
+ } finally {
436
+ repo.close()
437
+ }
438
+ } catch (e: Exception) {
439
+ result.put("ok", false)
440
+ result.put("error", e.message ?: "Unknown bundle error")
441
+ android.util.Log.e("GitBundle", "Bundle creation failed: ${e.message}", e)
442
+ }
443
+ return result.toString()
444
+ }
445
+
341
446
  // ─── Git fetch (JGit) ───────────────────────────────────────────
342
447
 
343
448
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-tiddlywiki-filesystem-android-external-storage",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "Expo native module for TidGi-Mobile: filesystem I/O + TiddlyWiki .tid/.meta/.json batch parsing + git status in Kotlin (Android) and Swift (iOS)",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",