@trap_stevo/filetide 0.0.85 → 0.0.87
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/SCL-1.0-Universal.md +228 -0
- package/dist/cjs/FileMessagerClient.js +134 -70
- package/dist/cjs/FileTide.js +163 -82
- package/dist/cjs/{FileMessager.js → FileTideAnchor.js} +464 -264
- package/dist/cjs/HUDManagers/EndpointManager.js +157 -0
- package/dist/cjs/HUDManagers/{FileNetClientManager.js → FileTideAnchorClientManager.js} +3 -2
- package/dist/cjs/HUDManagers/{FileNetUtilityManager.js → FileTideAnchorUtilityManager.js} +2 -2
- package/dist/cjs/HUDManagers/FileTideDebugUtilityManager.js +6 -0
- package/dist/cjs/HUDManagers/FileTideNetworkTrafficManager.js +419 -0
- package/dist/cjs/HUDManagers/FileTransferManager.js +90 -157
- package/dist/cjs/HUDManagers/FileTransferReceiverManager.js +169 -9
- package/dist/cjs/HUDManagers/FileTransferStatsManager.js +138 -0
- package/dist/cjs/HUDManagers/FileTransferTrackingManager.js +22 -0
- package/package.json +4 -5
- package/LICENSE.md +0 -79
- package/dist/cjs/HUDComponents/ConsoleProgressBarManager.js +0 -288
|
@@ -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
|
|
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
|
-
|
|
53
|
+
FileTideAnchorClientManager
|
|
53
54
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const {
|
|
4
4
|
HUDUtilityManager
|
|
5
5
|
} = require("@trap_stevo/legendarybuilderpronodejs-utilities");
|
|
6
|
-
class
|
|
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
|
-
|
|
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;
|