Skip to content

Commit 8c53300

Browse files
authored
Merge pull request #16 from CodePandaaAI/jvm-impl
Improve multi-file TCP batching and clean up local tooling files
2 parents 8878d5c + 292c6c4 commit 8c53300

12 files changed

Lines changed: 81 additions & 164 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ guide/
4343
_CODEX_CONTEXT_DO_NOT_DELETE/
4444
recover.py
4545
skills-main/
46+
skills-lock.json
4647

4748
# JavaScript
4849
node_modules/

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1515
- Text offers, receiver Accept/Decline, text transfer, Copy, and Clear.
1616
- Android and Desktop multiple-file selection and metadata offers.
1717
- Raw TCP file transfer using one persistent connection per accepted batch.
18-
- Sequential file framing with index/size validation and per-file save acknowledgements.
18+
- Sequential file framing with index/size validation and one final batch result containing receiver success and the completed-file count.
1919
- Android Downloads writing through pending `MediaStore` entries.
2020
- Desktop Downloads writing through temporary `.part` files and collision-safe final names.
2121
- Unified send operation states and best-effort cancellation.
@@ -29,6 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
2929
- Replaced the old generated sync implementation with a smaller, manually understood flow.
3030
- Separated Ktor HTTP offer/control messages from raw TCP file bytes.
3131
- Reused one TCP connection for the complete accepted multi-file batch instead of opening one connection per file.
32+
- Removed per-file flush-and-acknowledgement waits so an accepted batch can stream continuously before one final receiver result.
3233
- Positioned the project around direct local-network nearby sharing rather than chat or cloud sync.
3334

3435
### Known limitations

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Sync360 has a working Android-to-Android MVP for text and multiple-file transfer
5555
- Stream file bytes directly over raw TCP without loading an entire file into memory.
5656
- Save received files into public Android Downloads through `MediaStore`, preserving the extension when duplicate names are resolved.
5757
- Delete the incomplete current file if its receive operation fails or is cancelled.
58-
- Send files sequentially with a save acknowledgement after each file.
58+
- Stream each accepted file batch continuously, then confirm the batch with one final receiver result.
5959
- Cancel a pending send or active file transfer on a best-effort basis.
6060
- Show batch-wide byte percentage while files are being sent and received.
6161
- Show clear offer, transfer, success, failure, and cancelled states on the sender, with incoming, receiving, and received states on the receiver.
@@ -122,10 +122,10 @@ Platform file picker
122122
-> platform FileTransferSender opens an InputStream
123123
-> one raw TCP connection streams the accepted file batch
124124
-> platform DownloadsWriter saves each file
125-
-> receiver acknowledges that the file was saved
125+
-> receiver returns final success and completed-file count
126126
```
127127

128-
One TCP socket is opened for the complete accepted batch. Each file begins with its index and promised byte count, followed by exactly that many bytes. The receiver checks the index and size against the accepted offer before saving the file, then sends one save acknowledgement before the sender continues. The current shared payload buffer is 512 KiB; TCP correctness does not depend on sender and receiver reads using identical chunk boundaries.
128+
One TCP socket is opened for the complete accepted batch. Each file begins with its index and promised byte count, followed by exactly that many bytes. The receiver checks the index and size against the accepted offer before saving each file. The sender writes every file sequentially, flushes once after the complete batch, then reads one final success flag and completed-file count from the receiver. The count increases only after the platform Downloads writer successfully returns. The current shared payload buffer is 512 KiB; exact byte counts define file boundaries, so correctness does not depend on `flush()` calls or matching sender and receiver read chunks.
129129

130130
Files are sent sequentially. If a later file fails, files that were already completed stay in Downloads; the incomplete current file is cleaned up. Android uses a pending `MediaStore` entry and resolves its MIME type from the filename extension so duplicate names remain in the form `file (1).ext`. Desktop writes a temporary `.part` file before moving a completed file into place without overwriting an existing name.
131131

context.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,12 @@ Raw TCP is the file data plane:
5151

5252
```text
5353
one connection per accepted batch
54-
-> file index
55-
-> promised byte count
56-
-> exact file bytes
57-
-> save acknowledgement
58-
-> next file
54+
-> repeat for each file:
55+
-> file index
56+
-> promised byte count
57+
-> exact file bytes
58+
-> sender flushes once
59+
-> receiver returns final success and completed-file count
5960
```
6061

6162
Current shared transfer constants use a 512 KiB payload buffer, 5-second connect timeout, 60-second connected-socket timeout, and 10-second wait for the first file connection after acceptance.

docs/ARCHITECTURE.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,16 @@ Accepted file bytes use a separate raw TCP connection:
101101

102102
```text
103103
one connection for the accepted batch
104-
-> file index: Int
105-
-> promised file size: Long
106-
-> exactly promised-size bytes
107-
-> receiver save acknowledgement: Boolean
108-
-> repeat for the next file
104+
-> repeat for each accepted file:
105+
-> file index: Int
106+
-> promised file size: Long
107+
-> exactly promised-size bytes
108+
-> sender flushes after the complete batch
109+
-> receiver result: Boolean
110+
-> completed-file count: Int
109111
```
110112

111-
Files remain sequential. The receiver verifies each index and size against the accepted offer before saving. It acknowledges a file only after the platform Downloads writer completes it.
113+
Files remain sequential. The receiver verifies each index and size directly against the matching file in the accepted offer before saving. It increments the completed-file count only after the platform Downloads writer returns successfully. After every file has been processed, the receiver sends one final success flag and completed count. If processing fails, it attempts to send `false` with the number of files that were fully saved.
112114

113115
`FileTransferConstants` currently provides:
114116

@@ -117,7 +119,7 @@ Files remain sequential. The receiver verifies each index and size against the a
117119
- 60-second connected-socket timeout
118120
- 10-second wait for the first file connection after acceptance
119121

120-
The sender and receiver do not need matching read boundaries because TCP is a byte stream; exact file sizes define the protocol framing.
122+
The sender and receiver do not need matching read boundaries because TCP is a byte stream; exact file sizes define the protocol framing. Flushing once after the batch makes any remaining buffered bytes available before the sender waits for the final result, but the flush does not define file boundaries.
121123

122124
## Platform storage
123125

docs/ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Sync360 is an active Android-first rebuild. The current MVP can discover nearby
1111
- Android and Desktop multiple-file selection.
1212
- File metadata offer before any file bytes are sent.
1313
- One persistent raw TCP connection per accepted file batch.
14-
- Sequential file framing, index/size validation, and per-file save acknowledgements.
14+
- Sequential file framing, index/size validation, and one final success/completed-count result per batch.
1515
- Android public Downloads writing with incomplete-entry cleanup.
1616
- Desktop Downloads writing through temporary `.part` files and collision-safe final names.
1717
- Best-effort sender cancellation.

shared/src/androidMain/kotlin/com/liftley/sync360/data/network/tcp/AndroidFileTransferReceiver.kt

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ class AndroidFileTransferReceiver(
3232
@Volatile
3333
private var expectedFileOffer: FileOfferRequest? = null
3434

35-
private var nextExpectedFileIndex: Int = 0
3635
private var onFileSaved: ((completedFileCount: Int) -> Unit)? = null
3736
private var onProgress: ((FileTransferProgress) -> Unit)? = null
3837
private var onTransferFinished: ((wasSuccessful: Boolean) -> Unit)? = null
@@ -75,7 +74,6 @@ class AndroidFileTransferReceiver(
7574
onTransferFinished: (wasSuccessful: Boolean) -> Unit
7675
) {
7776
expectedFileOffer = fileOffer
78-
nextExpectedFileIndex = 0
7977
this.onFileSaved = onFileSaved
8078
this.onProgress = onProgress
8179
this.onTransferFinished = onTransferFinished
@@ -86,7 +84,6 @@ class AndroidFileTransferReceiver(
8684
override fun clearExpectedTransfer() {
8785
waitingForSenderTimeout?.cancel()
8886
expectedFileOffer = null
89-
nextExpectedFileIndex = 0
9087
onFileSaved = null
9188
onProgress = null
9289
onTransferFinished = null
@@ -107,6 +104,8 @@ class AndroidFileTransferReceiver(
107104

108105
val socketOutput = DataOutputStream(socket.getOutputStream())
109106

107+
var completedFileCount = 0
108+
110109
try {
111110
val fileOffer = expectedFileOffer
112111
?: error("No accepted file offer is waiting")
@@ -119,20 +118,16 @@ class AndroidFileTransferReceiver(
119118
val receivedFileIndex = socketInput.readInt()
120119
val receivedFileSize = socketInput.readLong()
121120

122-
if (receivedFileIndex != nextExpectedFileIndex) {
121+
if (receivedFileIndex != expectedFile.index) {
123122
error(
124-
"Expected file index $nextExpectedFileIndex " +
125-
"but received $receivedFileIndex"
123+
"Expected file index ${expectedFile.index}, " +
124+
"but received $receivedFileIndex"
126125
)
127126
}
128127

129-
if (expectedFile.index != receivedFileIndex) {
130-
error("File index does not match the accepted offer")
131-
}
132-
133128
val expectedFileSize = expectedFile.fileSizeBytes
134129

135-
if (receivedFileSize != expectedFileSize) {
130+
if (expectedFileSize != receivedFileSize) {
136131
error("File size does not match the accepted offer")
137132
}
138133

@@ -144,47 +139,35 @@ class AndroidFileTransferReceiver(
144139
onBytesWritten = progressTracker::addBytes
145140
)
146141

147-
markCurrentFileComplete(
148-
completedFileIndex = receivedFileIndex
149-
)
142+
completedFileCount++
150143

151-
sendSaveResult(socketOutput, wasSaved = true)
144+
onFileSaved?.invoke(completedFileCount)
152145
}
146+
socketOutput.writeBoolean(true)
147+
socketOutput.writeInt(completedFileCount)
148+
socketOutput.flush()
153149

154150
finishTransfer(wasSuccessful = true)
155151
} catch (exception: Exception) {
156152
exception.printStackTrace()
157153

158-
try {
159-
sendSaveResult(socketOutput, wasSaved = false)
160-
} catch (_: Exception) {
161-
// The sender may already have closed the socket.
154+
runCatching {
155+
socketOutput.writeBoolean(false)
156+
socketOutput.writeInt(completedFileCount)
157+
socketOutput.flush()
162158
}
163159

164160
finishTransfer(wasSuccessful = false)
165161
}
166162
}
167163
}
168164

169-
@Synchronized
170-
private fun markCurrentFileComplete(
171-
completedFileIndex: Int
172-
) {
173-
if (completedFileIndex != nextExpectedFileIndex) {
174-
return
175-
}
176-
177-
nextExpectedFileIndex++
178-
onFileSaved?.invoke(nextExpectedFileIndex)
179-
}
180-
181165
@Synchronized
182166
private fun finishTransfer(wasSuccessful: Boolean) {
183167
val completionCallback = onTransferFinished
184168

185169
waitingForSenderTimeout?.cancel()
186170
expectedFileOffer = null
187-
nextExpectedFileIndex = 0
188171
onFileSaved = null
189172
onProgress = null
190173
onTransferFinished = null
@@ -202,12 +185,4 @@ class AndroidFileTransferReceiver(
202185
finishTransfer(wasSuccessful = false)
203186
}
204187
}
205-
206-
private fun sendSaveResult(
207-
output: DataOutputStream,
208-
wasSaved: Boolean
209-
) {
210-
output.writeBoolean(wasSaved)
211-
output.flush()
212-
}
213188
}

shared/src/androidMain/kotlin/com/liftley/sync360/data/network/tcp/AndroidFileTransferSender.kt

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,20 @@ class AndroidFileTransferSender(
6666
fileIndex = fileIndex,
6767
file = file,
6868
socketOutput = socketOutput,
69-
socketInput = socketInput,
7069
buffer = buffer,
7170
progressTracker = progressTracker
7271
)
7372
}
73+
74+
// Ensure every remaining buffered byte reaches the receiver.
75+
socketOutput.flush()
76+
77+
val receiverSavedTransferSuccessfully = socketInput.readBoolean()
78+
val completedFileCount = socketInput.readInt()
79+
80+
check(receiverSavedTransferSuccessfully && completedFileCount == files.size) {
81+
"Receiver saved $completedFileCount of ${files.size} files"
82+
}
7483
} finally {
7584
activeSocket.compareAndSet(socket, null)
7685
}
@@ -89,7 +98,6 @@ class AndroidFileTransferSender(
8998
fileIndex: Int,
9099
file: SelectedFile,
91100
socketOutput: DataOutputStream,
92-
socketInput: DataInputStream,
93101
buffer: ByteArray,
94102
progressTracker: FileTransferProgressTracker
95103
) {
@@ -132,14 +140,6 @@ class AndroidFileTransferSender(
132140
bytesRemaining -= bytesRead
133141
progressTracker.addBytes(bytesRead)
134142
}
135-
136-
socketOutput.flush()
137-
138-
val receiverSavedFile = socketInput.readBoolean()
139-
140-
if (!receiverSavedFile) {
141-
error("Receiver could not save ${file.displayName}")
142-
}
143143
}
144144
}
145145

@@ -166,5 +166,4 @@ class AndroidFileTransferSender(
166166

167167
throw lastFailure ?: error("No address is available for ${device.deviceName}")
168168
}
169-
170169
}

shared/src/commonMain/composeResources/drawable/compose-multiplatform.xml

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)