@trap_stevo/filetide 0.0.85 → 0.0.86

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.
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const os = require("os");
5
+ class EndpointManager {
6
+ constructor(options = {}) {
7
+ this.baseUrl = this.cleanBaseUrl(options.baseUrl || process.env.FILETIDE_BACKEND_URL);
8
+ this.licenseKey = options.licenseKey || process.env.FILETIDE_LICENSE_KEY;
9
+ this.endpointId = options.endpointId || process.env.FILETIDE_ENDPOINT_ID || os.hostname();
10
+ this.endpointSecret = options.endpointSecret || process.env.FILETIDE_ENDPOINT_SECRET;
11
+ this.routesPath = options.routesPath || process.env.FILETIDE_ROUTES_PATH || '/api/filetide/routes';
12
+ this.timeoutMs = Number(options.timeoutMs || 15000);
13
+ this.routes = {
14
+ health: '/status',
15
+ registerEndpoint: '/api/endpoints/register',
16
+ heartbeat: '/api/endpoints/heartbeat',
17
+ resolveTransferPolicy: '/api/transfers/policy',
18
+ recordTransfer: '/api/transfers/usage',
19
+ quoteMonthToDate: '/api/billing/quote',
20
+ finalizeInvoice: '/api/billing/finalize'
21
+ };
22
+ if (!this.baseUrl) {
23
+ throw new Error('Missing FILETIDE_BACKEND_URL');
24
+ }
25
+ if (!this.licenseKey) {
26
+ throw new Error('Missing FILETIDE_LICENSE_KEY');
27
+ }
28
+ }
29
+ cleanBaseUrl(url) {
30
+ if (!url) return '';
31
+ return String(url).replace(/\/+$/, '');
32
+ }
33
+ buildUrl(path) {
34
+ if (!path.startsWith('/')) path = `/${path}`;
35
+ return `${this.baseUrl}${path}`;
36
+ }
37
+ makeAuthHeaders(body = '') {
38
+ const timestamp = new Date().toISOString();
39
+ const signature = this.endpointSecret ? crypto.createHmac('sha256', this.endpointSecret).update(`${timestamp}.${body}`).digest('hex') : '';
40
+ return {
41
+ 'content-type': 'application/json',
42
+ 'x-filetide-license-key': this.licenseKey,
43
+ 'x-filetide-endpoint-id': this.endpointId,
44
+ 'x-filetide-timestamp': timestamp,
45
+ ...(signature ? {
46
+ 'x-filetide-signature': signature
47
+ } : {})
48
+ };
49
+ }
50
+ async request(method, path, payload = undefined) {
51
+ const body = payload === undefined ? '' : JSON.stringify(payload);
52
+ const controller = new AbortController();
53
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
54
+ try {
55
+ const response = await fetch(this.buildUrl(path), {
56
+ method,
57
+ headers: this.makeAuthHeaders(body),
58
+ body: method === 'GET' ? undefined : body,
59
+ signal: controller.signal
60
+ });
61
+ const text = await response.text();
62
+ let data = null;
63
+ try {
64
+ data = text ? JSON.parse(text) : null;
65
+ } catch {
66
+ data = text;
67
+ }
68
+ if (!response.ok) {
69
+ const error = new Error(`FileTide backend request failed: ${method} ${path} ${response.status}`);
70
+ error.status = response.status;
71
+ error.data = data;
72
+ throw error;
73
+ }
74
+ return data;
75
+ } finally {
76
+ clearTimeout(timeout);
77
+ }
78
+ }
79
+ async discoverRoutes() {
80
+ try {
81
+ const data = await this.request('GET', this.routesPath);
82
+ if (data && typeof data === 'object') {
83
+ const discoveredRoutes = data.routes || data;
84
+ this.routes = {
85
+ ...this.routes,
86
+ ...discoveredRoutes
87
+ };
88
+ }
89
+ return this.routes;
90
+ } catch (error) {
91
+ if (error.status === 404) {
92
+ return this.routes;
93
+ }
94
+ throw error;
95
+ }
96
+ }
97
+ async health() {
98
+ return this.request('GET', this.routes.health);
99
+ }
100
+ async registerEndpoint(extra = {}) {
101
+ return this.request('POST', this.routes.registerEndpoint, {
102
+ licenseKey: this.licenseKey,
103
+ endpointId: this.endpointId,
104
+ hostname: os.hostname(),
105
+ platform: os.platform(),
106
+ arch: os.arch(),
107
+ uptimeSeconds: os.uptime(),
108
+ networkInterfaces: os.networkInterfaces(),
109
+ ...extra
110
+ });
111
+ }
112
+ async heartbeat(extra = {}) {
113
+ return this.request('POST', this.routes.heartbeat, {
114
+ licenseKey: this.licenseKey,
115
+ endpointId: this.endpointId,
116
+ hostname: os.hostname(),
117
+ uptimeSeconds: os.uptime(),
118
+ memory: {
119
+ total: os.totalmem(),
120
+ free: os.freemem()
121
+ },
122
+ loadAverage: os.loadavg(),
123
+ timestamp: new Date().toISOString(),
124
+ ...extra
125
+ });
126
+ }
127
+ async resolveTransferPolicy(input = {}) {
128
+ return this.request('POST', this.routes.resolveTransferPolicy, {
129
+ licenseKey: this.licenseKey,
130
+ endpointId: this.endpointId,
131
+ ...input
132
+ });
133
+ }
134
+ async recordTransfer(input = {}) {
135
+ return this.request('POST', this.routes.recordTransfer, {
136
+ licenseKey: this.licenseKey,
137
+ endpointId: this.endpointId,
138
+ ...input
139
+ });
140
+ }
141
+ async quoteMonthToDate(input = {}) {
142
+ return this.request('POST', this.routes.quoteMonthToDate, {
143
+ licenseKey: this.licenseKey,
144
+ endpointId: this.endpointId,
145
+ ...input
146
+ });
147
+ }
148
+ async finalizeInvoice(input = {}) {
149
+ return this.request('POST', this.routes.finalizeInvoice, {
150
+ licenseKey: this.licenseKey,
151
+ endpointId: this.endpointId,
152
+ ...input
153
+ });
154
+ }
155
+ }
156
+ ;
157
+ module.exports = EndpointManager;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  const currentOnlineClients = new Map();
4
- class FileNetClientManager {
4
+ class FileTideAnchorClientManager {
5
5
  static addOnlineClient(clientID, pClientID, tideID, id, details = {}) {
6
6
  if (currentOnlineClients.get(pClientID)) {
7
7
  currentOnlineClients.delete(pClientID);
@@ -48,6 +48,7 @@ class FileNetClientManager {
48
48
  return currentOnlineClients;
49
49
  }
50
50
  }
51
+ ;
51
52
  module.exports = {
52
- FileNetClientManager
53
+ FileTideAnchorClientManager
53
54
  };
@@ -3,7 +3,7 @@
3
3
  const {
4
4
  HUDUtilityManager
5
5
  } = require("@trap_stevo/legendarybuilderpronodejs-utilities");
6
- class FileNetUtilityManager {
6
+ class FileTideAnchorUtilityManager {
7
7
  static convertSeconds(seconds) {
8
8
  if (seconds === 0) return "0 s";
9
9
  const units = [{
@@ -67,5 +67,5 @@ class FileNetUtilityManager {
67
67
  }
68
68
  }
69
69
  module.exports = {
70
- FileNetUtilityManager
70
+ FileTideAnchorUtilityManager
71
71
  };
@@ -8,6 +8,7 @@ const significantMessageColors = ["#00C897", "#00E0FF"];
8
8
  const noticeMessageColors = ["#4A90E2", "#A3D8F4"];
9
9
  const infoAccentMessageColors = ["#6B768F", "#8D99AB"];
10
10
  const errorMessageColors = ["#F94144", "#F3722C"];
11
+ const debugMessageColors = ["#8B5CF6", "#C084FC"];
11
12
  const infoMessageColors = ["#028090", "#56cfe1"];
12
13
  const logHandler = {
13
14
  "significant": function (includeDate = false, ...message) {
@@ -25,6 +26,10 @@ const logHandler = {
25
26
  "info": function (includeDate = false, ...message) {
26
27
  const log = includeDate ? `[${new Date().toLocaleString()}] ~ ` : "";
27
28
  outputGradient(log + message.join(" "), infoMessageColors);
29
+ },
30
+ "debug": function (includeDate = false, ...message) {
31
+ const log = includeDate ? `[${new Date().toLocaleString()}] ~ ` : "";
32
+ outputGradient(log + message.join(" "), debugMessageColors);
28
33
  }
29
34
  };
30
35
  function gradientText(text, colors, options = {
@@ -107,6 +112,7 @@ module.exports = {
107
112
  significantMessageColors,
108
113
  noticeMessageColors,
109
114
  errorMessageColors,
115
+ debugMessageColors,
110
116
  infoAccentMessageColors,
111
117
  infoMessageColors
112
118
  };
@@ -0,0 +1,419 @@
1
+ "use strict";
2
+
3
+ var EventEmitter = require('events').EventEmitter;
4
+ var EVENTS = Object.freeze({
5
+ NETWORK_AVAILABLE: 'network:available',
6
+ NETWORK_CONGESTED: 'network:congested',
7
+ NETWORK_UNSTABLE: 'network:unstable',
8
+ TRANSFER_PAUSE: 'transfer:pause',
9
+ TRANSFER_RESUME: 'transfer:resume',
10
+ CHUNK_SEND: 'chunk:send',
11
+ CHUNK_DELAY: 'chunk:delay',
12
+ CHUNK_RESIZE: 'chunk:resize',
13
+ BANDWIDTH_UPDATED: 'bandwidth:updated'
14
+ });
15
+ var STATES = Object.freeze({
16
+ UNKNOWN: 'unknown',
17
+ AVAILABLE: 'available',
18
+ CONGESTED: 'congested',
19
+ UNSTABLE: 'unstable',
20
+ OFFLINE: 'offline'
21
+ });
22
+ var DEFAULTS = Object.freeze({
23
+ defaultBandwidthBps: 1024 * 1024,
24
+ minimumBandwidthBps: 32 * 1024,
25
+ maximumBandwidthBps: 1024 * 1024 * 1024,
26
+ targetUtilization: 0.9,
27
+ congestedUtilizationFactor: 0.65,
28
+ targetChunkDurationMs: 250,
29
+ minChunkSize: 16 * 1024,
30
+ maxChunkSize: 1024 * 1024,
31
+ burstSeconds: 0.25,
32
+ congestedLatencyMs: 250,
33
+ unstableLatencyMs: 1000,
34
+ congestedFailureRate: 0.1,
35
+ unstableFailureRate: 0.3,
36
+ sampleWindowSize: 20,
37
+ smoothingFactor: 0.25,
38
+ throughputProbeIntervalMs: 2000,
39
+ throughputProbeGrowthFactor: 1.10,
40
+ healthyThroughputRatio: 0.90,
41
+ degradedThroughputRatio: 0.65
42
+ });
43
+ function clamp(value, minimum, maximum) {
44
+ return Math.min(Math.max(value, minimum), maximum);
45
+ }
46
+ function assertPositiveNumber(value, name) {
47
+ if (typeof value !== 'number' || !isFinite(value) || value <= 0) {
48
+ throw new TypeError(name + " expects a positive number");
49
+ }
50
+ }
51
+ function mergeOptions(options) {
52
+ var merged = {};
53
+ var key;
54
+ for (key in DEFAULTS) {
55
+ if (Object.prototype.hasOwnProperty.call(DEFAULTS, key)) {
56
+ merged[key] = DEFAULTS[key];
57
+ }
58
+ }
59
+ options = options || {};
60
+ for (key in options) {
61
+ if (key !== 'clock' && Object.prototype.hasOwnProperty.call(options, key)) {
62
+ merged[key] = options[key];
63
+ }
64
+ }
65
+ return merged;
66
+ }
67
+ function FileTideNetworkTrafficManager(options) {
68
+ EventEmitter.call(this);
69
+ options = options || {};
70
+ this._config = mergeOptions(options);
71
+ this._clock = typeof options.clock === 'function' ? options.clock : Date.now;
72
+ this._state = STATES.UNKNOWN;
73
+ this._paused = false;
74
+ this._manualPause = false;
75
+ this._autoPaused = false;
76
+ this._pauseReason = null;
77
+ this._bandwidthBps = null;
78
+ this._deliveredThroughputBps = null;
79
+ this._latencyMs = null;
80
+ this._samples = [];
81
+ this._lastThroughputProbeAt = 0;
82
+ this._tokens = this._getBucketCapacity();
83
+ this._lastRefillAt = this._clock();
84
+ this._validateConfiguration();
85
+ }
86
+ FileTideNetworkTrafficManager.prototype = Object.create(EventEmitter.prototype);
87
+ FileTideNetworkTrafficManager.prototype.constructor = FileTideNetworkTrafficManager;
88
+ FileTideNetworkTrafficManager.prototype._validateConfiguration = function () {
89
+ assertPositiveNumber(this._config.defaultBandwidthBps, "defaultBandwidthBps");
90
+ assertPositiveNumber(this._config.minimumBandwidthBps, "minimumBandwidthBps");
91
+ assertPositiveNumber(this._config.targetChunkDurationMs, "targetChunkDurationMs");
92
+ assertPositiveNumber(this._config.minChunkSize, 'minChunkSize');
93
+ assertPositiveNumber(this._config.maxChunkSize, 'maxChunkSize');
94
+ assertPositiveNumber(this._config.burstSeconds, 'burstSeconds');
95
+ assertPositiveNumber(this._config.sampleWindowSize, 'sampleWindowSize');
96
+ if (this._config.targetUtilization <= 0 || this._config.targetUtilization > 1) {
97
+ throw new RangeError("targetUtilization expects greater than 0 and at most 1");
98
+ }
99
+ if (this._config.congestedUtilizationFactor <= 0 || this._config.congestedUtilizationFactor > 1) {
100
+ throw new RangeError("congestedUtilizationFactor expects greater than 0 and at most 1");
101
+ }
102
+ if (this._config.smoothingFactor <= 0 || this._config.smoothingFactor > 1) {
103
+ throw new RangeError("smoothingFactor expects greater than 0 and at most 1");
104
+ }
105
+ if (this._config.minChunkSize > this._config.maxChunkSize) {
106
+ throw new RangeError("minChunkSize cannot exceed maxChunkSize");
107
+ }
108
+ assertPositiveNumber(this._config.maximumBandwidthBps, "maximumBandwidthBps");
109
+ assertPositiveNumber(this._config.throughputProbeIntervalMs, "throughputProbeIntervalMs");
110
+ if (this._config.throughputProbeGrowthFactor <= 1) {
111
+ throw new RangeError("throughputProbeGrowthFactor expects greater than 1");
112
+ }
113
+ if (this._config.healthyThroughputRatio <= 0 || this._config.healthyThroughputRatio > 1) {
114
+ throw new RangeError("healthyThroughputRatio expects greater than 0 and at most 1");
115
+ }
116
+ if (this._config.degradedThroughputRatio <= 0 || this._config.degradedThroughputRatio >= this._config.healthyThroughputRatio) {
117
+ throw new RangeError("degradedThroughputRatio expects greater than 0 and less than healthyThroughputRatio");
118
+ }
119
+ };
120
+ FileTideNetworkTrafficManager.prototype._smooth = function (current, sample) {
121
+ if (current === null) {
122
+ return sample;
123
+ }
124
+ return this._config.smoothingFactor * sample + (1 - this._config.smoothingFactor) * current;
125
+ };
126
+ FileTideNetworkTrafficManager.prototype._getFailureRate = function () {
127
+ if (this._samples.length === 0) {
128
+ return 0;
129
+ }
130
+ var failures = this._samples.reduce(function (total, sample) {
131
+ return total + (sample.success ? 0 : 1);
132
+ }, 0);
133
+ return failures / this._samples.length;
134
+ };
135
+ FileTideNetworkTrafficManager.prototype._getRetryRate = function () {
136
+ if (this._samples.length === 0) {
137
+ return 0;
138
+ }
139
+ var retries = this._samples.reduce(function (total, sample) {
140
+ return total + (sample.retried ? 1 : 0);
141
+ }, 0);
142
+ return retries / this._samples.length;
143
+ };
144
+ FileTideNetworkTrafficManager.prototype._evaluateState = function (online) {
145
+ var failureRate = this._getFailureRate();
146
+ var retryRate = this._getRetryRate();
147
+ if (online === false) {
148
+ return STATES.OFFLINE;
149
+ }
150
+ if (this._latencyMs !== null && this._latencyMs >= this._config.unstableLatencyMs) {
151
+ return STATES.UNSTABLE;
152
+ }
153
+ if (failureRate >= this._config.unstableFailureRate) {
154
+ return STATES.UNSTABLE;
155
+ }
156
+ if (this._latencyMs !== null && this._latencyMs >= this._config.congestedLatencyMs) {
157
+ return STATES.CONGESTED;
158
+ }
159
+ if (failureRate >= this._config.congestedFailureRate || retryRate >= this._config.congestedFailureRate) {
160
+ return STATES.CONGESTED;
161
+ }
162
+ return STATES.AVAILABLE;
163
+ };
164
+ FileTideNetworkTrafficManager.prototype._setState = function (nextState) {
165
+ if (nextState === this._state) {
166
+ return;
167
+ }
168
+ var previousState = this._state;
169
+ this._state = nextState;
170
+ var eventPayload = {
171
+ state: nextState,
172
+ previousState: previousState,
173
+ status: this.getStatus()
174
+ };
175
+ if (nextState === STATES.AVAILABLE) {
176
+ this.emit(EVENTS.NETWORK_AVAILABLE, eventPayload);
177
+ if (this._autoPaused && !this._manualPause) {
178
+ this.resume('network-recovered', true);
179
+ }
180
+ return;
181
+ }
182
+ if (nextState === STATES.CONGESTED) {
183
+ this.emit(EVENTS.NETWORK_CONGESTED, eventPayload);
184
+ return;
185
+ }
186
+ this.emit(EVENTS.NETWORK_UNSTABLE, eventPayload);
187
+ this.pause(nextState === STATES.OFFLINE ? 'network-offline' : 'network-unstable', true);
188
+ };
189
+ FileTideNetworkTrafficManager.prototype.updateNetworkSample = function (sample) {
190
+ sample = sample || {};
191
+ if (sample.bandwidthBps !== undefined) {
192
+ assertPositiveNumber(sample.bandwidthBps, 'bandwidthBps');
193
+ this._refillTokens();
194
+ var previousCapacity = this._getBucketCapacity();
195
+ var fillRatio = clamp(this._tokens / previousCapacity, 0, 1);
196
+ this._bandwidthBps = this._smooth(this._bandwidthBps, sample.bandwidthBps);
197
+ this._tokens = fillRatio * this._getBucketCapacity();
198
+ this.emit(EVENTS.BANDWIDTH_UPDATED, {
199
+ bandwidthBps: this._bandwidthBps,
200
+ status: this.getStatus()
201
+ });
202
+ }
203
+ if (sample.latencyMs !== undefined) {
204
+ if (typeof sample.latencyMs !== 'number' || !isFinite(sample.latencyMs) || sample.latencyMs < 0) {
205
+ throw new TypeError("latencyMs expects a non-negative number");
206
+ }
207
+ this._latencyMs = this._smooth(this._latencyMs, sample.latencyMs);
208
+ }
209
+ if (sample.success !== undefined || sample.retried !== undefined) {
210
+ this._samples.push({
211
+ success: sample.success !== false,
212
+ retried: sample.retried === true,
213
+ timestamp: this._clock()
214
+ });
215
+ if (this._samples.length > this._config.sampleWindowSize) {
216
+ this._samples.shift();
217
+ }
218
+ }
219
+ this._setState(this._evaluateState(sample.online));
220
+ return this.getStatus();
221
+ };
222
+ FileTideNetworkTrafficManager.prototype.recordTransfer = function (result) {
223
+ result = result || {};
224
+ assertPositiveNumber(result.bytes, 'bytes');
225
+ assertPositiveNumber(result.durationMs, 'durationMs');
226
+ return this.updateNetworkSample({
227
+ bandwidthBps: result.bytes / (result.durationMs / 1000),
228
+ latencyMs: result.latencyMs,
229
+ online: result.online,
230
+ success: result.success,
231
+ retried: result.retried
232
+ });
233
+ };
234
+ FileTideNetworkTrafficManager.prototype.recordDeliveredThroughput = function (throughputBps) {
235
+ assertPositiveNumber(throughputBps, "throughputBps");
236
+ var now = this._clock();
237
+ this._deliveredThroughputBps = this._smooth(this._deliveredThroughputBps, throughputBps);
238
+ this._setState(this._evaluateState(true));
239
+ var currentBandwidth = this._bandwidthBps || this._config.defaultBandwidthBps;
240
+ var currentSendRate = this._getSendRate();
241
+ var throughputRatio = currentSendRate > 0 ? this._deliveredThroughputBps / currentSendRate : 0;
242
+ if (now - this._lastThroughputProbeAt < this._config.throughputProbeIntervalMs) {
243
+ return this.getStatus();
244
+ }
245
+ this._lastThroughputProbeAt = now;
246
+ var nextBandwidth = currentBandwidth;
247
+ if (this._state === STATES.AVAILABLE && throughputRatio >= this._config.healthyThroughputRatio) {
248
+ nextBandwidth = Math.min(currentBandwidth * this._config.throughputProbeGrowthFactor, this._config.maximumBandwidthBps);
249
+ } else if (throughputRatio < this._config.degradedThroughputRatio) {
250
+ var observedCapacity = this._deliveredThroughputBps / this._config.targetUtilization;
251
+ nextBandwidth = this._smooth(currentBandwidth, observedCapacity);
252
+ nextBandwidth = Math.max(nextBandwidth, this._config.minimumBandwidthBps);
253
+ }
254
+ nextBandwidth = Math.min(nextBandwidth, this._config.maximumBandwidthBps);
255
+ if (nextBandwidth !== currentBandwidth) {
256
+ this._refillTokens();
257
+ var previousCapacity = this._getBucketCapacity();
258
+ var fillRatio = clamp(this._tokens / previousCapacity, 0, 1);
259
+ this._bandwidthBps = nextBandwidth;
260
+ this._tokens = fillRatio * this._getBucketCapacity();
261
+ this.emit(EVENTS.BANDWIDTH_UPDATED, {
262
+ bandwidthBps: this._bandwidthBps,
263
+ deliveredThroughputBps: this._deliveredThroughputBps,
264
+ throughputRatio,
265
+ status: this.getStatus()
266
+ });
267
+ }
268
+ return this.getStatus();
269
+ };
270
+ FileTideNetworkTrafficManager.prototype.waitForChunkAdmission = function (requestedBytes) {
271
+ var self = this;
272
+ assertPositiveNumber(requestedBytes, "requestedBytes");
273
+ return new Promise(function (resolve) {
274
+ function evaluate() {
275
+ var decision = self.getChunkDecision(requestedBytes);
276
+ if (decision.action === "send") {
277
+ resolve(decision);
278
+ return;
279
+ }
280
+ if (decision.action === "pause") {
281
+ self.once(EVENTS.TRANSFER_RESUME, evaluate);
282
+ return;
283
+ }
284
+ setTimeout(evaluate, Math.max(decision.delayMs || 1, 1));
285
+ }
286
+ evaluate();
287
+ });
288
+ };
289
+ FileTideNetworkTrafficManager.prototype.pause = function (reason, automatic) {
290
+ if (automatic !== true) {
291
+ this._manualPause = true;
292
+ } else {
293
+ this._autoPaused = true;
294
+ }
295
+ this._pauseReason = reason || 'requested';
296
+ if (this._paused) {
297
+ return this.getStatus();
298
+ }
299
+ this._paused = true;
300
+ this.emit(EVENTS.TRANSFER_PAUSE, {
301
+ reason: this._pauseReason,
302
+ automatic: automatic === true,
303
+ status: this.getStatus()
304
+ });
305
+ return this.getStatus();
306
+ };
307
+ FileTideNetworkTrafficManager.prototype.resume = function (reason, automatic) {
308
+ if (automatic === true && this._manualPause) {
309
+ return this.getStatus();
310
+ }
311
+ if (automatic !== true) {
312
+ this._manualPause = false;
313
+ }
314
+ this._autoPaused = false;
315
+ this._pauseReason = null;
316
+ if (!this._paused) {
317
+ return this.getStatus();
318
+ }
319
+ this._paused = false;
320
+ this._lastRefillAt = this._clock();
321
+ this.emit(EVENTS.TRANSFER_RESUME, {
322
+ reason: reason || 'requested',
323
+ automatic: automatic === true,
324
+ status: this.getStatus()
325
+ });
326
+ return this.getStatus();
327
+ };
328
+ FileTideNetworkTrafficManager.prototype._getSendRate = function () {
329
+ var bandwidth = this._bandwidthBps || this._config.defaultBandwidthBps;
330
+ var rate = Math.max(bandwidth * this._config.targetUtilization, this._config.minimumBandwidthBps);
331
+ if (this._state === STATES.CONGESTED) {
332
+ rate *= this._config.congestedUtilizationFactor;
333
+ }
334
+ return rate;
335
+ };
336
+ FileTideNetworkTrafficManager.prototype._getBucketCapacity = function () {
337
+ var bandwidth = this._bandwidthBps || this._config.defaultBandwidthBps;
338
+ return Math.max(bandwidth * this._config.burstSeconds, this._config.maxChunkSize);
339
+ };
340
+ FileTideNetworkTrafficManager.prototype._refillTokens = function () {
341
+ var now = this._clock();
342
+ var elapsedMs = Math.max(now - this._lastRefillAt, 0);
343
+ var addedTokens = this._getSendRate() * (elapsedMs / 1000);
344
+ this._tokens = Math.min(this._tokens + addedTokens, this._getBucketCapacity());
345
+ this._lastRefillAt = now;
346
+ };
347
+ FileTideNetworkTrafficManager.prototype.getRecommendedChunkSize = function () {
348
+ var targetSize = this._getSendRate() * (this._config.targetChunkDurationMs / 1000);
349
+ return Math.round(clamp(targetSize, this._config.minChunkSize, this._config.maxChunkSize));
350
+ };
351
+ FileTideNetworkTrafficManager.prototype.getChunkDecision = function (requestedBytes) {
352
+ assertPositiveNumber(requestedBytes, 'requestedBytes');
353
+ if (this._paused) {
354
+ return {
355
+ action: 'pause',
356
+ allowed: false,
357
+ reason: this._pauseReason,
358
+ delayMs: null,
359
+ chunkSize: 0
360
+ };
361
+ }
362
+ this._refillTokens();
363
+ var recommendedSize = this.getRecommendedChunkSize();
364
+ var chunkSize = Math.min(requestedBytes, recommendedSize);
365
+ if (chunkSize !== requestedBytes) {
366
+ this.emit(EVENTS.CHUNK_RESIZE, {
367
+ requestedBytes: requestedBytes,
368
+ chunkSize: chunkSize,
369
+ status: this.getStatus()
370
+ });
371
+ }
372
+ if (this._tokens >= chunkSize) {
373
+ this._tokens -= chunkSize;
374
+ var sendDecision = {
375
+ action: 'send',
376
+ allowed: true,
377
+ reason: 'capacity-available',
378
+ delayMs: 0,
379
+ chunkSize: chunkSize
380
+ };
381
+ this.emit(EVENTS.CHUNK_SEND, sendDecision);
382
+ return sendDecision;
383
+ }
384
+ var delayMs = Math.ceil((chunkSize - this._tokens) / this._getSendRate() * 1000);
385
+ var delayDecision = {
386
+ action: 'delay',
387
+ allowed: false,
388
+ reason: 'bandwidth-throttled',
389
+ delayMs: Math.max(delayMs, 1),
390
+ chunkSize: chunkSize
391
+ };
392
+ this.emit(EVENTS.CHUNK_DELAY, delayDecision);
393
+ return delayDecision;
394
+ };
395
+ FileTideNetworkTrafficManager.prototype.canSendChunk = function (requestedBytes) {
396
+ return this.getChunkDecision(requestedBytes).allowed;
397
+ };
398
+ FileTideNetworkTrafficManager.prototype.getStatus = function () {
399
+ var sendRateBps = this._getSendRate();
400
+ return {
401
+ state: this._state,
402
+ paused: this._paused,
403
+ pauseReason: this._pauseReason,
404
+ bandwidthBps: this._bandwidthBps,
405
+ deliveredThroughputBps: this._deliveredThroughputBps,
406
+ latencyMs: this._latencyMs,
407
+ failureRate: this._getFailureRate(),
408
+ retryRate: this._getRetryRate(),
409
+ recommendedChunkSize: this.getRecommendedChunkSize(),
410
+ sendRateBps,
411
+ throughputRatio: this._deliveredThroughputBps !== null && sendRateBps > 0 ? this._deliveredThroughputBps / sendRateBps : null,
412
+ sampleCount: this._samples.length
413
+ };
414
+ };
415
+ var singleton = new FileTideNetworkTrafficManager();
416
+ module.exports = singleton;
417
+ module.exports.FileTideNetworkTrafficManager = FileTideNetworkTrafficManager;
418
+ module.exports.EVENTS = EVENTS;
419
+ module.exports.STATES = STATES;