teyyare 0.2.0 → 0.2.2

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.
package/bin/teyyare.js CHANGED
@@ -136,7 +136,7 @@ Protocol overhead:${snap.protocolOverheadPercent.toFixed(2)}%
136
136
  Peak RSS: ${(snap.peakRSS / (1024 * 1024)).toFixed(2)} MB
137
137
  `);
138
138
  } else if (command === 'status') {
139
- console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.1.0 status: ready');
139
+ console.log('\x1b[1m\x1b[36mTeyyare\x1b[0m v0.2.2 status: ready');
140
140
  } else {
141
141
  console.error(`Unknown command: ${command}`);
142
142
  printUsage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teyyare",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "High-performance resumable file transfer engine and CLI built on muttafa and raptiye",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Throttled terminal progress renderer.
3
+ * Intelligent, accurate metrics for both Upload (send) and Download (pull) modes.
3
4
  */
4
5
 
5
6
  export class ProgressReporter {
@@ -7,10 +8,12 @@ export class ProgressReporter {
7
8
  * @param {object} [options]
8
9
  * @param {number} [options.intervalMs=150]
9
10
  * @param {boolean} [options.enabled=true]
11
+ * @param {'sender'|'receiver'} [options.role='sender']
10
12
  */
11
- constructor({ intervalMs = 150, enabled = true } = {}) {
13
+ constructor({ intervalMs = 150, enabled = true, role = 'sender' } = {}) {
12
14
  this.intervalMs = intervalMs;
13
15
  this.enabled = enabled && Boolean(process.stdout.isTTY || process.env.CI !== 'true');
16
+ this.role = role;
14
17
 
15
18
  this.fileName = '';
16
19
  this.bytesDone = 0;
@@ -23,16 +26,20 @@ export class ProgressReporter {
23
26
  this.muttafaQueue = 0;
24
27
  this.raptiyeRTT = 0;
25
28
  this.tcpPressure = 0;
29
+ this.resumeHits = 0;
30
+ this.diskSpeedMBs = 0;
26
31
 
27
32
  this.startTime = Date.now();
28
33
  this.lastRender = 0;
29
34
  this._timer = null;
30
35
  this._linesRendered = 0;
36
+ this._speedHistory = [];
31
37
  }
32
38
 
33
39
  start() {
34
40
  if (!this.enabled) return;
35
41
  this.startTime = Date.now();
42
+ this._speedHistory = [{ time: this.startTime, bytes: this.bytesDone }];
36
43
  this._timer = setInterval(() => this.render(), this.intervalMs);
37
44
  }
38
45
 
@@ -52,13 +59,15 @@ export class ProgressReporter {
52
59
  if (state.bytesDone !== undefined) this.bytesDone = state.bytesDone;
53
60
  if (state.totalBytes !== undefined) this.totalBytes = state.totalBytes;
54
61
  if (state.chunksDone !== undefined) this.chunksDone = state.chunksDone;
55
- if (state.totalChunks !== undefined) this.totalChunks = state.totalChunks;
62
+ if (state.totalChunks !== undefined && state.totalChunks > 0) this.totalChunks = state.totalChunks;
56
63
  if (state.generationId !== undefined) this.generationId = state.generationId;
57
64
  if (state.inflightGen !== undefined) this.inflightGen = state.inflightGen;
58
65
  if (state.inflightBytes !== undefined) this.inflightBytes = state.inflightBytes;
59
66
  if (state.muttafaQueue !== undefined) this.muttafaQueue = state.muttafaQueue;
60
67
  if (state.raptiyeRTT !== undefined) this.raptiyeRTT = state.raptiyeRTT;
61
68
  if (state.tcpPressure !== undefined) this.tcpPressure = state.tcpPressure;
69
+ if (state.resumeHits !== undefined) this.resumeHits = state.resumeHits;
70
+ if (state.diskSpeedMBs !== undefined) this.diskSpeedMBs = state.diskSpeedMBs;
62
71
  }
63
72
 
64
73
  formatBytes(bytes) {
@@ -77,39 +86,75 @@ export class ProgressReporter {
77
86
  render() {
78
87
  if (!this.enabled) return;
79
88
 
80
- const elapsedSec = Math.max(0.001, (Date.now() - this.startTime) / 1000);
81
- const throughputMBs = (this.bytesDone / (1024 * 1024)) / elapsedSec;
89
+ const now = Date.now();
90
+ const elapsedSec = Math.max(0.001, (now - this.startTime) / 1000);
91
+
92
+ // Track sliding window (last 2 seconds) for smooth, responsive current speed
93
+ this._speedHistory.push({ time: now, bytes: this.bytesDone });
94
+ while (this._speedHistory.length > 2 && (now - this._speedHistory[0].time) > 2000) {
95
+ this._speedHistory.shift();
96
+ }
97
+ const oldest = this._speedHistory[0];
98
+ const windowSec = Math.max(0.001, (now - oldest.time) / 1000);
99
+ const windowBytes = Math.max(0, this.bytesDone - oldest.bytes);
100
+
101
+ const avgSpeedMBs = (this.bytesDone / (1024 * 1024)) / elapsedSec;
102
+ const currentSpeedMBs = windowSec >= 0.4 ? (windowBytes / (1024 * 1024)) / windowSec : avgSpeedMBs;
103
+ const effectiveSpeed = currentSpeedMBs > 0.05 ? currentSpeedMBs : avgSpeedMBs;
104
+
82
105
  const percent = this.totalBytes > 0 ? ((this.bytesDone / this.totalBytes) * 100).toFixed(1) : '0.0';
83
106
 
84
107
  let etaStr = '--:--';
85
- if (throughputMBs > 0 && this.totalBytes > this.bytesDone) {
108
+ if (effectiveSpeed > 0 && this.totalBytes > this.bytesDone) {
86
109
  const remainingBytes = this.totalBytes - this.bytesDone;
87
- const remainingSec = Math.round(remainingBytes / (throughputMBs * 1024 * 1024));
110
+ const remainingSec = Math.round(remainingBytes / (effectiveSpeed * 1024 * 1024));
88
111
  const mm = String(Math.floor(remainingSec / 60)).padStart(2, '0');
89
112
  const ss = String(remainingSec % 60).padStart(2, '0');
90
113
  etaStr = `${mm}:${ss}`;
91
114
  }
92
115
 
93
- const lines = [
94
- '\x1b[1m\x1b[36mTeyyare\x1b[0m',
95
- '',
96
- `\x1b[1m${this.fileName || 'Transferring...'}\x1b[0m`,
97
- '',
98
- `${this.formatBytes(this.bytesDone)} / ${this.formatBytes(this.totalBytes)}`,
99
- `\x1b[32m${percent}%\x1b[0m`,
100
- '',
101
- `throughput \x1b[1m${throughputMBs.toFixed(1)} MB/s\x1b[0m`,
102
- `ETA ${etaStr}`,
103
- '',
104
- `chunks ${this.chunksDone} / ${this.totalChunks}`,
105
- `generation ${this.generationId}`,
106
- `inflight gen ${this.inflightGen}`,
107
- `inflight bytes ${this.formatBytes(this.inflightBytes)}`,
108
- '',
109
- `muttafa queue ${this.muttafaQueue}`,
110
- `raptiye RTT ${this.raptiyeRTT} ms`,
111
- `TCP pressure ${this.tcpPressure}`
112
- ];
116
+ let lines;
117
+ if (this.role === 'receiver') {
118
+ const diskStr = this.diskSpeedMBs > 0 ? `${this.diskSpeedMBs.toFixed(1)} MB/s` : `${effectiveSpeed.toFixed(1)} MB/s`;
119
+ lines = [
120
+ '\x1b[1m\x1b[36mTeyyare [Download]\x1b[0m',
121
+ '',
122
+ `\x1b[1m${this.fileName || 'Transferring...'}\x1b[0m`,
123
+ '',
124
+ `${this.formatBytes(this.bytesDone)} / ${this.formatBytes(this.totalBytes)}`,
125
+ `\x1b[32m${percent}%\x1b[0m`,
126
+ '',
127
+ `network speed \x1b[1m${effectiveSpeed.toFixed(1)} MB/s\x1b[0m`,
128
+ `disk speed \x1b[1m${diskStr}\x1b[0m`,
129
+ `ETA ${etaStr}`,
130
+ '',
131
+ `chunks ${this.chunksDone} / ${this.totalChunks || this.chunksDone}`,
132
+ `generation ${this.generationId}`,
133
+ `verified CRC32 ${this.chunksDone}`,
134
+ `resume hits ${this.resumeHits}`
135
+ ];
136
+ } else {
137
+ lines = [
138
+ '\x1b[1m\x1b[36mTeyyare [Upload]\x1b[0m',
139
+ '',
140
+ `\x1b[1m${this.fileName || 'Transferring...'}\x1b[0m`,
141
+ '',
142
+ `${this.formatBytes(this.bytesDone)} / ${this.formatBytes(this.totalBytes)}`,
143
+ `\x1b[32m${percent}%\x1b[0m`,
144
+ '',
145
+ `throughput \x1b[1m${effectiveSpeed.toFixed(1)} MB/s\x1b[0m`,
146
+ `ETA ${etaStr}`,
147
+ '',
148
+ `chunks ${this.chunksDone} / ${this.totalChunks || this.chunksDone}`,
149
+ `generation ${this.generationId}`,
150
+ `inflight gen ${this.inflightGen}`,
151
+ `inflight bytes ${this.formatBytes(this.inflightBytes)}`,
152
+ '',
153
+ `muttafa queue ${this.muttafaQueue}`,
154
+ `raptiye RTT ${this.raptiyeRTT} ms`,
155
+ `TCP pressure ${this.tcpPressure}`
156
+ ];
157
+ }
113
158
 
114
159
  if (process.stdout.isTTY) {
115
160
  if (this._linesRendered > 0) {
@@ -118,8 +163,7 @@ export class ProgressReporter {
118
163
  process.stdout.write(lines.join('\n') + '\n');
119
164
  this._linesRendered = lines.length;
120
165
  } else {
121
- // Non-interactive log
122
- process.stdout.write(`[Teyyare] ${percent}% | ${throughputMBs.toFixed(1)} MB/s | Chunks: ${this.chunksDone}/${this.totalChunks} | ETA: ${etaStr}\n`);
166
+ process.stdout.write(`[Teyyare] ${percent}% | ${effectiveSpeed.toFixed(1)} MB/s | Chunks: ${this.chunksDone}/${this.totalChunks || this.chunksDone} | ETA: ${etaStr}\n`);
123
167
  }
124
168
  }
125
169
  }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Zero-copy generation framing and buffer extraction utilities.
3
+ * Ensures complete compatibility across all versions of Muttafa and Raptiye.
4
+ */
5
+
6
+ /**
7
+ * Extract scatter/gather buffers [metaBuf, payloadBuf] from any Muttafa generation.
8
+ * @param {object} batch
9
+ * @returns {Uint8Array[]}
10
+ */
11
+ export function extractGenerationBuffers(batch) {
12
+ if (typeof batch.buffers === 'function') {
13
+ return batch.buffers();
14
+ }
15
+
16
+ const store = batch.store || batch;
17
+ if (typeof store.buffers === 'function') {
18
+ return store.buffers();
19
+ }
20
+
21
+ // Direct extraction from ArenaStore
22
+ const rawBuf = store._allocator?.rawBuffer;
23
+ const payload = rawBuf ? rawBuf.subarray(0, store._allocator.usedBytes) : new Uint8Array(0);
24
+ const metaBuf = new Uint8Array(store.length * 25);
25
+ const view = new DataView(metaBuf.buffer, metaBuf.byteOffset, metaBuf.byteLength);
26
+
27
+ let off = 0;
28
+ const chunks = store._chunks || [];
29
+ for (let c = 0; c < chunks.length; c++) {
30
+ const chunk = chunks[c];
31
+ for (let i = 0; i < chunk.count; i++) {
32
+ view.setFloat64(off, chunk.seqs[i], true);
33
+ view.setUint8(off + 8, chunk.types[i]);
34
+ view.setUint32(off + 9, chunk.keyOffsets[i], true);
35
+ view.setUint32(off + 13, chunk.keyLengths[i], true);
36
+ view.setUint32(off + 17, chunk.valOffsets[i], true);
37
+ view.setUint32(off + 21, chunk.valLengths[i], true);
38
+ off += 25;
39
+ }
40
+ }
41
+
42
+ return [metaBuf, payload];
43
+ }
44
+
45
+ /**
46
+ * Fast zero-copy generator / unpacker for received generation buffers.
47
+ * @param {Uint8Array} metaBuf
48
+ * @param {Uint8Array} payloadBuf
49
+ * @returns {Array<{ seq: number, type: number, key: Uint8Array, val: Uint8Array }>}
50
+ */
51
+ export function unpackGeneration(metaBuf, payloadBuf) {
52
+ const entryCount = (metaBuf.byteLength / 25) | 0;
53
+ const view = new DataView(metaBuf.buffer, metaBuf.byteOffset, metaBuf.byteLength);
54
+ const entries = new Array(entryCount);
55
+
56
+ let off = 0;
57
+ for (let i = 0; i < entryCount; i++) {
58
+ const seq = view.getFloat64(off, true);
59
+ const type = view.getUint8(off + 8);
60
+ const keyOffset = view.getUint32(off + 9, true);
61
+ const keyLength = view.getUint32(off + 13, true);
62
+ const valOffset = view.getUint32(off + 17, true);
63
+ const valLength = view.getUint32(off + 21, true);
64
+ off += 25;
65
+
66
+ entries[i] = {
67
+ seq,
68
+ type,
69
+ key: payloadBuf.subarray(keyOffset, keyOffset + keyLength),
70
+ val: payloadBuf.subarray(valOffset, valOffset + valLength)
71
+ };
72
+ }
73
+
74
+ return entries;
75
+ }
76
+
77
+ /**
78
+ * Pack a single-mutation batch directly into scatter/gather wire buffers.
79
+ * @param {number} generationId
80
+ * @param {number} opcode
81
+ * @param {Uint8Array} key
82
+ * @param {Uint8Array} [val]
83
+ * @returns {object}
84
+ */
85
+ export function packSingleMutationBatch(generationId, opcode, key, val = new Uint8Array(0)) {
86
+ const metaBuf = new Uint8Array(25);
87
+ const view = new DataView(metaBuf.buffer, metaBuf.byteOffset, 25);
88
+ view.setFloat64(0, 1, true); // seq
89
+ view.setUint8(8, opcode);
90
+ view.setUint32(9, 0, true); // keyOffset
91
+ view.setUint32(13, key.byteLength, true); // keyLength
92
+ view.setUint32(17, key.byteLength, true); // valOffset
93
+ view.setUint32(21, val.byteLength, true); // valLength
94
+
95
+ const payloadBuf = new Uint8Array(key.byteLength + val.byteLength);
96
+ payloadBuf.set(key, 0);
97
+ payloadBuf.set(val, key.byteLength);
98
+
99
+ return {
100
+ generationId,
101
+ firstSequence: 1,
102
+ lastSequence: 1,
103
+ entryCount: 1,
104
+ byteLength: metaBuf.byteLength + payloadBuf.byteLength,
105
+ buffers: [metaBuf, payloadBuf]
106
+ };
107
+ }
package/src/index.js CHANGED
@@ -4,7 +4,6 @@
4
4
 
5
5
  import path from 'node:path';
6
6
  import { existsSync } from 'node:fs';
7
- import { MutableBuffer, ArenaStore, BumpAllocator, FrozenArenaStore } from 'muttafa';
8
7
  import { TCPTransport, ReplicationPipeline } from 'raptiye';
9
8
  import { SenderPipeline } from './sender/pipeline.js';
10
9
  import { ReceiverPipeline } from './receiver/pipeline.js';
@@ -12,6 +11,7 @@ import { TransferManifest } from './manifest/manifest.js';
12
11
  import { FileResumeState } from './resume/state.js';
13
12
  import { MetricsCollector } from './metrics/collector.js';
14
13
  import { ProgressReporter } from './cli/progress.js';
14
+ import { packSingleMutationBatch, unpackGeneration } from './core/generation.js';
15
15
  import {
16
16
  Opcodes,
17
17
  packChunkMeta,
@@ -39,22 +39,9 @@ export {
39
39
  };
40
40
 
41
41
  async function submitSingleMutation(replication, opcode, key, value = new Uint8Array(0)) {
42
- const store = new ArenaStore({ allocator: new BumpAllocator(16 * 1024) });
43
- const mutable = new MutableBuffer(Date.now() & 0x7fffffff, store);
44
- mutable.append(1, opcode, key, value);
45
- const frozen = mutable.freeze();
46
- frozen.markFlushing();
47
- const receipt = await replication.submitBatch({
48
- generationId: frozen.generation,
49
- firstSequence: frozen.firstSequence,
50
- lastSequence: frozen.lastSequence,
51
- entryCount: frozen.entryCount,
52
- byteLength: frozen.byteLength,
53
- buffers: frozen.buffers()
54
- });
42
+ const batch = packSingleMutationBatch(Date.now() & 0x7fffffff, opcode, key, value);
43
+ const receipt = await replication.submitBatch(batch);
55
44
  await receipt.delivered();
56
- frozen.markFlushed();
57
- frozen.release();
58
45
  }
59
46
 
60
47
  /**
@@ -102,9 +89,9 @@ export async function serve({
102
89
  replication.onBatch(async (msg) => {
103
90
  if (!msg.buffers || msg.buffers.length < 2) return;
104
91
  const [metaBuf, payloadBuf] = msg.buffers;
105
- const store = FrozenArenaStore.fromBuffers(metaBuf, payloadBuf);
106
- if (store.length > 0 && store.getType(0) === Opcodes.PULL_REQUEST) {
107
- const { remotePath } = unpackPullRequest(store.getKey(0));
92
+ const entries = unpackGeneration(metaBuf, payloadBuf);
93
+ if (entries.length > 0 && entries[0].type === Opcodes.PULL_REQUEST) {
94
+ const { remotePath } = unpackPullRequest(entries[0].key);
108
95
  let resolved = path.isAbsolute(remotePath) ? remotePath : path.resolve(destinationDir, remotePath);
109
96
  if (!existsSync(resolved)) {
110
97
  const alt = path.resolve(destinationDir, remotePath);
@@ -126,11 +113,21 @@ export async function serve({
126
113
  manifest,
127
114
  metrics
128
115
  });
129
- await sender.transfer();
116
+ setImmediate(async () => {
117
+ try {
118
+ await sender.transfer();
119
+ } catch (err) {
120
+ await submitSingleMutation(
121
+ replication,
122
+ Opcodes.PULL_ERROR,
123
+ packPullError({ errorCode: 500, message: err.message })
124
+ );
125
+ }
126
+ });
130
127
  return;
131
128
  }
132
129
 
133
- await receiver._processGenerationBatch(msg);
130
+ await receiver.enqueueBatch(msg);
134
131
  });
135
132
 
136
133
  return {
@@ -165,7 +162,7 @@ export async function send({
165
162
  knownResumeState = new Map()
166
163
  }) {
167
164
  const metrics = new MetricsCollector();
168
- const progress = showProgress ? new ProgressReporter() : null;
165
+ const progress = showProgress ? new ProgressReporter({ role: 'sender' }) : null;
169
166
 
170
167
  // Ephemeral sender port
171
168
  const senderPort = 10000 + Math.floor(Math.random() * 20000);
@@ -227,7 +224,7 @@ export async function pull({
227
224
  showProgress = true
228
225
  }) {
229
226
  const metrics = new MetricsCollector();
230
- const progress = showProgress ? new ProgressReporter() : null;
227
+ const progress = showProgress ? new ProgressReporter({ role: 'receiver' }) : null;
231
228
  if (progress) progress.start();
232
229
 
233
230
  const clientPort = 10000 + Math.floor(Math.random() * 20000);
@@ -8,9 +8,9 @@ import fs from 'node:fs/promises';
8
8
  import { existsSync, createReadStream } from 'node:fs';
9
9
  import path from 'node:path';
10
10
  import crypto from 'node:crypto';
11
- import { FrozenArenaStore } from 'muttafa';
12
11
  import { crc32 } from 'raptiye';
13
12
  import { Opcodes, unpackChunkMeta, unpackFileBegin, unpackTransferBegin, unpackFileEnd, unpackPullError } from '../core/protocol.js';
13
+ import { unpackGeneration } from '../core/generation.js';
14
14
  import { FileResumeState } from '../resume/state.js';
15
15
 
16
16
  export class ReceiverPipeline {
@@ -42,14 +42,58 @@ export class ReceiverPipeline {
42
42
  this._finishResolvers = [];
43
43
  this._errorRejecters = [];
44
44
 
45
+ // Processing queue to ensure strictly ordered, serialized generation processing
46
+ this._batchQueue = [];
47
+ this._processingQueue = false;
48
+
45
49
  // Hook up Raptiye batch handler if autoHookBatch is true
46
50
  if (this.replication && autoHookBatch) {
47
51
  this.replication.onBatch(async (msg) => {
48
- await this._processGenerationBatch(msg);
52
+ return this.enqueueBatch(msg);
49
53
  });
50
54
  }
51
55
  }
52
56
 
57
+ /**
58
+ * Enqueue incoming generation batch to guarantee strict FIFO sequential processing.
59
+ * @param {object} msg
60
+ * @returns {Promise<void>}
61
+ */
62
+ enqueueBatch(msg) {
63
+ return new Promise((resolve, reject) => {
64
+ this._batchQueue.push({ msg, resolve, reject });
65
+ this._drainQueue();
66
+ });
67
+ }
68
+
69
+ async _drainQueue() {
70
+ if (this._processingQueue) return;
71
+ this._processingQueue = true;
72
+
73
+ try {
74
+ while (this._batchQueue.length > 0) {
75
+ const item = this._batchQueue.shift();
76
+ try {
77
+ await this._processGenerationBatch(item.msg);
78
+ item.resolve();
79
+ } catch (err) {
80
+ this.transferError = err;
81
+ for (const reject of this._errorRejecters) {
82
+ reject(err);
83
+ }
84
+ this._finishResolvers = [];
85
+ this._errorRejecters = [];
86
+ item.reject(err);
87
+ }
88
+ }
89
+ } finally {
90
+ this._processingQueue = false;
91
+ if (this._batchQueue.length > 0) {
92
+ this._drainQueue();
93
+ }
94
+ }
95
+ }
96
+
53
97
  async waitForCompletion() {
54
98
  if (this.transferError) throw this.transferError;
55
99
  if (this.transferFinished) return;
@@ -70,14 +114,18 @@ export class ReceiverPipeline {
70
114
  this.metrics.generationBytes += (metaBuf.byteLength + payloadBuf.byteLength);
71
115
  }
72
116
 
73
- // Zero-copy reconstruction of Muttafa generation
74
- const store = FrozenArenaStore.fromBuffers(metaBuf, payloadBuf);
75
- const count = store.length;
117
+ if (this.progress) {
118
+ this.progress.update({
119
+ generationId: msg.generationId || (this.metrics ? this.metrics.generations : 0)
120
+ });
121
+ }
122
+
123
+ // Zero-copy unpacking of generation entries
124
+ const entries = unpackGeneration(metaBuf, payloadBuf);
125
+ const count = entries.length;
76
126
 
77
127
  for (let i = 0; i < count; i++) {
78
- const type = store.getType(i);
79
- const key = store.getKey(i);
80
- const val = store.getValue(i);
128
+ const { type, key, val } = entries[i];
81
129
 
82
130
  if (this.metrics) {
83
131
  this.metrics.mutations++;
@@ -166,9 +214,12 @@ export class ReceiverPipeline {
166
214
  });
167
215
 
168
216
  if (this.progress) {
217
+ const fileSize = Number(info.fileSize);
218
+ const estimatedChunks = Math.max(1, Math.ceil(fileSize / (1024 * 1024)));
169
219
  this.progress.update({
170
220
  fileName: info.path,
171
- totalBytes: Number(info.fileSize)
221
+ totalBytes: fileSize,
222
+ totalChunks: estimatedChunks
172
223
  });
173
224
  }
174
225
  }
@@ -218,10 +269,18 @@ export class ReceiverPipeline {
218
269
  }
219
270
 
220
271
  if (this.progress) {
272
+ const diskTimeSec = Math.max(0.001, (this.metrics ? this.metrics.diskWriteTimeMs : 1) / 1000);
273
+ const writtenMB = (this.metrics ? this.metrics.fileBytesWritten : 0) / (1024 * 1024);
274
+ const diskMBs = Number((writtenMB / diskTimeSec).toFixed(1));
275
+ file.maxChunkSize = Math.max(file.maxChunkSize || 0, meta.length);
276
+ const realTotalChunks = file.maxChunkSize > 0 && file.fileSize > 0 ? Math.ceil(file.fileSize / file.maxChunkSize) : 0;
221
277
  this.progress.update({
222
278
  fileName: file.relativePath,
223
279
  bytesDone: this.metrics ? this.metrics.fileBytesWritten : 0,
224
- chunksDone: this.metrics ? this.metrics.chunksVerified : 0
280
+ chunksDone: this.metrics ? this.metrics.chunksVerified : 0,
281
+ totalChunks: realTotalChunks > 0 ? realTotalChunks : undefined,
282
+ resumeHits: this.metrics ? this.metrics.resumeHits : 0,
283
+ diskSpeedMBs: diskMBs
225
284
  });
226
285
  }
227
286
 
@@ -17,6 +17,8 @@ import {
17
17
  packTransferBegin,
18
18
  packTransferEnd
19
19
  } from '../core/protocol.js';
20
+ import { extractGenerationBuffers } from '../core/generation.js';
21
+ import { FileResumeState } from '../resume/state.js';
20
22
 
21
23
  class RaptiyeSink extends Sink {
22
24
  /**
@@ -36,20 +38,23 @@ class RaptiyeSink extends Sink {
36
38
  * @param {import('muttafa').ImmutableBuffer} batch
37
39
  */
38
40
  async flush(batch) {
39
- const buffers = batch.buffers();
41
+ const buffers = extractGenerationBuffers(batch);
40
42
  const t0 = Date.now();
43
+ const generationId = batch.generationId || batch.generation || 1;
44
+ const entryCount = batch.entryCount || batch.store?.length || 1;
45
+ const byteLength = batch.byteLength || (buffers[0].byteLength + buffers[1].byteLength);
41
46
 
42
47
  if (this.metrics) {
43
48
  this.metrics.generations++;
44
- this.metrics.generationBytes += batch.byteLength;
49
+ this.metrics.generationBytes += byteLength;
45
50
  }
46
51
 
47
52
  const receipt = await this.replication.submitBatch({
48
- generationId: batch.generationId,
49
- firstSequence: batch.firstSequence,
50
- lastSequence: batch.lastSequence,
51
- entryCount: batch.entryCount,
52
- byteLength: batch.byteLength,
53
+ generationId,
54
+ firstSequence: batch.firstSequence || 1,
55
+ lastSequence: batch.lastSequence || 1,
56
+ entryCount,
57
+ byteLength,
53
58
  buffers
54
59
  });
55
60
 
@@ -99,7 +104,8 @@ export class SenderPipeline {
99
104
  this.chunkSize = options.chunkSize || 1024 * 1024;
100
105
  this.maxEntries = options.maxEntries || 8;
101
106
  this.maxBytes = options.maxBytes || 8 * 1024 * 1024;
102
- this.maxImmutableBuffers = options.maxImmutableBuffers || 4;
107
+ this.maxImmutableBuffers = options.maxImmutableBuffers || 8;
108
+ this.maxConcurrentFlushes = options.maxConcurrentFlushes || 4;
103
109
  this.metrics = metrics;
104
110
  this.progress = progress;
105
111
  this.knownResumeState = knownResumeState;
@@ -120,6 +126,8 @@ export class SenderPipeline {
120
126
  maxEntries: this.maxEntries,
121
127
  maxBytes: this.maxBytes,
122
128
  maxImmutableBuffers: this.maxImmutableBuffers,
129
+ maxConcurrentFlushes: this.maxConcurrentFlushes,
130
+ ordered: true,
123
131
  backpressurePolicy: BackpressurePolicy.WAIT,
124
132
  onBackpressure: (active) => {
125
133
  if (this.metrics && active) {