(db: Database, remote: SyncRPC, backend?: string)
| 290 | // successful push. The wire shape mirrors pullOnce in reverse: |
| 291 | // stage bytes the remote lacks, then push the entry stream. |
| 292 | export async function pushOnce(db: Database, remote: SyncRPC, backend?: string): Promise<number> { |
| 293 | const sincePush = readWatermark(db, "pushRev", backend); |
| 294 | const localRev = currentRev(db); |
| 295 | if (localRev <= sincePush) return 0; |
| 296 | |
| 297 | const entries: ChangeEntry[] = []; |
| 298 | const wantedHashes: Uint8Array[] = []; |
| 299 | const seenHash = new Set<string>(); |
| 300 | for await (const e of coalesceChanges(db, { rev: sincePush, path: null })) { |
| 301 | entries.push(e); |
| 302 | if (e.kind === "file") { |
| 303 | for (const c of e.chunks) { |
| 304 | const k = hex(c.hash); |
| 305 | if (!seenHash.has(k)) { |
| 306 | seenHash.add(k); |
| 307 | wantedHashes.push(c.hash); |
| 308 | } |
| 309 | } |
| 310 | } |
| 311 | } |
| 312 | if (entries.length === 0) return 0; |
| 313 | |
| 314 | // Probe the remote for the chunks it already holds; ship the |
| 315 | // complement. |
| 316 | const remoteHas = new Set<string>(); |
| 317 | if (wantedHashes.length > 0) { |
| 318 | const have = await remote.hasObjects(wantedHashes); |
| 319 | for (const h of have) remoteHas.add(hex(h)); |
| 320 | } |
| 321 | const missing = wantedHashes.filter((h) => !remoteHas.has(hex(h))); |
| 322 | |
| 323 | if (missing.length > 0) { |
| 324 | const local = (function* () { |
| 325 | for (const h of missing) { |
| 326 | const row = db.one<{ bytes: Uint8Array }>( |
| 327 | "SELECT bytes FROM vfs_blob_bytes WHERE hash = ?", |
| 328 | h, |
| 329 | ); |
| 330 | if (row === undefined) { |
| 331 | throw new Error(`pushOnce: missing local blob ${hex(h)}`); |
| 332 | } |
| 333 | yield { hash: h, bytes: row.bytes }; |
| 334 | } |
| 335 | })(); |
| 336 | const bytesStream = new ReadableStream<{ hash: Uint8Array; bytes: Uint8Array }>({ |
| 337 | pull(controller) { |
| 338 | const next = local.next(); |
| 339 | if (next.done) controller.close(); |
| 340 | else controller.enqueue(next.value); |
| 341 | }, |
| 342 | }); |
| 343 | await remote.pushObjects(bytesStream); |
| 344 | } |
| 345 | |
| 346 | const entryStream = new ReadableStream<ChangeEntry>({ |
| 347 | start(controller) { |
| 348 | for (const e of entries) controller.enqueue(e); |
| 349 | controller.close(); |
no test coverage detected