teyyare 0.2.1 → 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.1",
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
  }
package/src/index.js CHANGED
@@ -113,11 +113,21 @@ export async function serve({
113
113
  manifest,
114
114
  metrics
115
115
  });
116
- 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
+ });
117
127
  return;
118
128
  }
119
129
 
120
- await receiver._processGenerationBatch(msg);
130
+ await receiver.enqueueBatch(msg);
121
131
  });
122
132
 
123
133
  return {
@@ -152,7 +162,7 @@ export async function send({
152
162
  knownResumeState = new Map()
153
163
  }) {
154
164
  const metrics = new MetricsCollector();
155
- const progress = showProgress ? new ProgressReporter() : null;
165
+ const progress = showProgress ? new ProgressReporter({ role: 'sender' }) : null;
156
166
 
157
167
  // Ephemeral sender port
158
168
  const senderPort = 10000 + Math.floor(Math.random() * 20000);
@@ -214,7 +224,7 @@ export async function pull({
214
224
  showProgress = true
215
225
  }) {
216
226
  const metrics = new MetricsCollector();
217
- const progress = showProgress ? new ProgressReporter() : null;
227
+ const progress = showProgress ? new ProgressReporter({ role: 'receiver' }) : null;
218
228
  if (progress) progress.start();
219
229
 
220
230
  const clientPort = 10000 + Math.floor(Math.random() * 20000);
@@ -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,6 +114,12 @@ export class ReceiverPipeline {
70
114
  this.metrics.generationBytes += (metaBuf.byteLength + payloadBuf.byteLength);
71
115
  }
72
116
 
117
+ if (this.progress) {
118
+ this.progress.update({
119
+ generationId: msg.generationId || (this.metrics ? this.metrics.generations : 0)
120
+ });
121
+ }
122
+
73
123
  // Zero-copy unpacking of generation entries
74
124
  const entries = unpackGeneration(metaBuf, payloadBuf);
75
125
  const count = entries.length;
@@ -164,9 +214,12 @@ export class ReceiverPipeline {
164
214
  });
165
215
 
166
216
  if (this.progress) {
217
+ const fileSize = Number(info.fileSize);
218
+ const estimatedChunks = Math.max(1, Math.ceil(fileSize / (1024 * 1024)));
167
219
  this.progress.update({
168
220
  fileName: info.path,
169
- totalBytes: Number(info.fileSize)
221
+ totalBytes: fileSize,
222
+ totalChunks: estimatedChunks
170
223
  });
171
224
  }
172
225
  }
@@ -216,10 +269,18 @@ export class ReceiverPipeline {
216
269
  }
217
270
 
218
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;
219
277
  this.progress.update({
220
278
  fileName: file.relativePath,
221
279
  bytesDone: this.metrics ? this.metrics.fileBytesWritten : 0,
222
- 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
223
284
  });
224
285
  }
225
286
 
@@ -104,7 +104,8 @@ export class SenderPipeline {
104
104
  this.chunkSize = options.chunkSize || 1024 * 1024;
105
105
  this.maxEntries = options.maxEntries || 8;
106
106
  this.maxBytes = options.maxBytes || 8 * 1024 * 1024;
107
- this.maxImmutableBuffers = options.maxImmutableBuffers || 4;
107
+ this.maxImmutableBuffers = options.maxImmutableBuffers || 8;
108
+ this.maxConcurrentFlushes = options.maxConcurrentFlushes || 4;
108
109
  this.metrics = metrics;
109
110
  this.progress = progress;
110
111
  this.knownResumeState = knownResumeState;
@@ -125,6 +126,8 @@ export class SenderPipeline {
125
126
  maxEntries: this.maxEntries,
126
127
  maxBytes: this.maxBytes,
127
128
  maxImmutableBuffers: this.maxImmutableBuffers,
129
+ maxConcurrentFlushes: this.maxConcurrentFlushes,
130
+ ordered: true,
128
131
  backpressurePolicy: BackpressurePolicy.WAIT,
129
132
  onBackpressure: (active) => {
130
133
  if (this.metrics && active) {