1/* 2 * Copyright (C) 2015 Shane Carr and others 3 * X11 License 4 * 5 * Permission is hereby granted, free of charge, to any person obtaining a 6 * copy of this software and associated documentation files (the "Software"), 7 * to deal in the Software without restriction, including without limitation 8 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 * and/or sell copies of the Software, and to permit persons to whom the 10 * Software is furnished to do so, subject to the following conditions: 11 * 12 * The above copyright notice and this permission notice shall be included in 13 * all copies or substantial portions of the Software. 14 * 15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 * DEALINGS IN THE SOFTWARE. 22 * 23 * Except as contained in this notice, the names of the authors or copyright 24 * holders shall not be used in advertising or otherwise to promote the sale, 25 * use or other dealings in this Software without prior written authorization 26 * from the authors or copyright holders. 27 */ 28 29// Do not check function indentation because this is intentionally ignored in order to preserve history in git. 30/* eslint-disable indent */ 31 32/* 33 * A client-side JavaScript object to handle file uploads to a Node.JS server 34 * via Socket.IO. 35 * @implements EventTarget 36 * @param {SocketIO} socket The current Socket.IO connection. 37 */ 38(function (scope, name, factory) { 39 /* eslint-disable no-undef */ 40 if (typeof define === "function" && define.amd) { 41 define([], factory); 42 } 43 else if (typeof module === "object" && module.exports) { 44 module.exports = factory(); 45 } 46 else { 47 scope[name] = factory(); 48 } 49 /* eslint-enable no-undef */ 50}(this, "SocketIOFileUpload", function () { 51 return function (socket, options) { 52 "use strict"; 53 54 var self = this; // avoids context issues 55
56 // Check for compatibility 57 if (!window.File || !window.FileReader) { 58 throw new Error("Socket.IO File Upload: Browser Not Supported"); 59 } 60 61 if ( !window.siofu_global ) { 62 window.siofu_global = { 63 instances: 0, 64 downloads: 0 65 }; 66 } 67 68 // Private and Public Variables 69 var callbacks = {}, 70 uploadedFiles = {}, 71 chunkCallbacks = {}, 72 readyCallbacks = {}, 73 communicators = {}; 74 75 var _getOption = function (key, defaultValue) { 76 if(!options) { 77 return defaultValue; 78 } 79 return options[key] || defaultValue; 80 }; 81 82 self.fileInputElementId = "siofu_input_"+window.siofu_global.instances++; 83 self.resetFileInputs = true; 84 self.useText = _getOption("useText", false); 85 self.serializedOctets = _getOption("serializedOctets", false); 86 self.useBuffer = _getOption("useBuffer", true); 87 self.chunkSize = _getOption("chunkSize", 1024 * 100); // 100kb default chunk size 88 self.topicName = _getOption("topicName", "siofu"); 89 90 /** 91 * WrapData allow you to wrap the Siofu messages into a predefined format. 92 * You can then easily use Siofu packages even in strongly typed topic. 93 * wrapData can be a boolean or an object. It is false by default. 94 * If wrapData is true it will allow you to send all the messages to only one topic by wrapping the siofu actions and messages. 95 * 96 * ex: 97 { 98 action: 'complete', 99 message: { 100 id: id, 101 success: success, 102 detail: fileInfo.clientDetail 103 } 104 } 105 *
106 * If wrapData is an object constituted of two mandatory key and one optional: 107 * wrapKey and unwrapKey (mandatory): Corresponding to the key used to wrap the siofu data and message 108 * additionalData (optional): Corresponding to the data to send along with file data 109 * 110 * ex: 111 * if wrapData = { 112 wrapKey: { 113 action: 'actionType', 114 message: 'data' 115 }, 116 unwrapKey: { 117 action: 'actionType', 118 message: 'message' 119 }, 120 additionalData: { 121 acknowledgement: true 122 } 123 } 124 * When Siofu will send for example a complete message this will send: 125 * 126 { 127 acknowledgement: true, 128 actionType: 'complete', 129 data: { 130 id: id, 131 success: success, 132 detail: fileInfo.clientDetail 133 } 134 } 135 * and it's waiting from client data formatted like this: 136 * 137 { 138 actionType: '...', 139 message: {...} 140 } 141 * /!\ If wrapData is wrong configured is interpreted as false /!\ 142 */ 143 self.wrapData = _getOption("wrapData", false); 144 145 var _isWrapDataWellConfigured = function () { 146 if (typeof self.wrapData === "boolean") { 147 return true; 148 } 149 if (typeof self.wrapData !== "object" || Array.isArray(self.wrapData)) { 150 return false; 151 } 152 153 if(!self.wrapData.wrapKey || typeof self.wrapData.wrapKey.action !== "string" || typeof self.wrapData.wrapKey.message !== "string" || 154 !self.wrapData.unwrapKey || typeof self.wrapData.unwrapKey.action !== "string" || typeof self.wrapData.unwrapKey.message !== "string") { 155 return false; 156 } 157 158 return true; 159 }; 160 161 162 /** 163 * Allow user to access to some private function to customize message reception. 164 * This is used if you specified wrapOptions on the client side and have to manually bind message to callback. 165 */ 166 self.exposePrivateFunction = _getOption("exposePrivateFunction", false); 167 168 var _getTopicName = function (topicExtension) { 169 if (self.wrapData) { 170 return self.topicName; 171 } 172 173 return self.topicName + topicExtension; 174 }; 175 176 var _wrapData = function (data, action) { 177 if(!_isWrapDataWellConfigured() || !self.wrapData) { 178 return data; 179 } 180 var dataWrapped = {}; 181 if(self.wrapData.additionalData) { 182 Object.assign(dataWrapped, self.wrapData.additionalData); 183 } 184 185 var actionKey = self.wrapData.wrapKey && typeof self.wrapData.wrapKey.action === "string" ? self.wrapData.wrapKey.action : "action"; 186 var messageKey = self.wrapData.wrapKey && typeof self.wrapData.wrapKey.message === "string" ? self.wrapData.wrapKey.message : "message"; 187 188 dataWrapped[actionKey] = action; 189 dataWrapped[messageKey] = data; 190 return dataWrapped; 191 }; 192 193 /** 194 * Private method to dispatch a custom event on the instance. 195 * @param {string} eventName Name for which listeners can listen. 196 * @param {object} properties An object literal with additional properties 197 * to be attached to the event object. 198 * @return {boolean} false if any callback returned false; true otherwise 199 */ 200 var _dispatch = function (eventName, properties) { 201 var evnt = document.createEvent("Event"); 202 evnt.initEvent(eventName, false, false); 203 for (var prop in properties) { 204 if (properties.hasOwnProperty(prop)) { 205 evnt[prop] = properties[prop]; 206 } 207 } 208 return self.dispatchEvent(evnt); 209 }; 210 211 /** 212 * Private method to bind an event listener. Useful to ensure that all 213 * events have been unbound. Inspired by Backbone.js. 214 */ 215 var _listenedReferences = []; 216 var _listenTo = function (object, eventName, callback, bubble) { 217 object.addEventListener(eventName, callback, bubble); 218 _listenedReferences.push(arguments); 219 }; 220 var _stopListeningTo = function (object, eventName, callback, bubble) { 221 if (object.removeEventListener) { 222 object.removeEventListener(eventName, callback, bubble); 223 } 224 }; 225 var _stopListening = function () { 226 for (var i = _listenedReferences.length - 1; i >= 0; i--) { 227 _stopListeningTo.apply(this, _listenedReferences[i]); 228 } 229 _listenedReferences = []; 230 }; 231 232 /** 233 * Private closure for the _load function. 234 * @param {File} file A W3C File object 235 * @return {void} 236 */ 237 var _loadOne = function (file) { 238 // First check for file size 239 if (self.maxFileSize !== null && file.size > self.maxFileSize) { 240 _dispatch("error", { 241 file: file, 242 message: "Attempt by client to upload file exceeding the maximum file size", 243 code: 1 244 }); 245 return; 246 } 247 248 // Dispatch an event to listeners and stop now if they don't want 249 // this file to be uploaded.
250 var evntResult = _dispatch("start", { 251 file: file 252 }); 253 if (!evntResult) return; 254 255 // Scope variables 256 var reader = new FileReader(), 257 id = window.siofu_global.downloads++, 258 uploadComplete = false, 259 useText = self.useText, 260 offset = 0, 261 newName; 262 if (reader._realReader) reader = reader._realReader; // Support Android Crosswalk 263 uploadedFiles[id] = file; 264 265 // An object for the outside to use to communicate with us 266 var communicator = { id: id }; 267 268 // Calculate chunk size 269 var chunkSize = self.chunkSize; 270 if (chunkSize >= file.size || chunkSize <= 0) chunkSize = file.size; 271 272 // Private function to handle transmission of file data 273 var transmitPart = function (start, end, content) { 274 var isBase64 = false; 275 if (!useText) { 276 try { 277 var uintArr = new Uint8Array(content); 278 279 // Support the transmission of serialized ArrayBuffers 280 // for experimental purposes, but default to encoding the 281 // transmission in Base 64. 282 if (self.serializedOctets) { 283 content = uintArr; 284 } 285 else if (self.useBuffer) { 286 content = uintArr.buffer; 287 } 288 else { 289 isBase64 = true; 290 content = _uint8ArrayToBase64(uintArr); 291 } 292 } 293 catch (error) { 294 socket.emit(_getTopicName("_done"), _wrapData({ 295 id: id, 296 interrupt: true 297 }, "done")); 298 return; 299 } 300 } 301 302 // TODO override the send data 303 socket.emit(_getTopicName("_progress"), _wrapData({ 304 id: id, 305 size: file.size, 306 start: start, 307 end: end, 308 content: content, 309 base64: isBase64 310 }, "progress")); 311 }; 312 313 // Callback when tranmission is complete. 314 var transmitDone = function () { 315 socket.emit(_getTopicName("_done"), _wrapData({ 316 id: id 317 }, "done")); 318 }; 319 320 // Load a "chunk" of the file from offset to offset+chunkSize. 321 // 322 // Note that FileReader has its own "progress" event. However, 323 // it has not proven to be reliable enough for production. See 324 // Stack Overflow question #16713386. 325 // 326 // To compensate, we will manually load the file in chunks of a 327 // size specified by the user in the uploader.chunkSize property. 328 var processChunk = function () { 329 // Abort if we are told to do so. 330 if (communicator.abort) return; 331 332 var chunk = file.slice(offset, Math.min(offset+chunkSize, file.size)); 333 if (useText) { 334 reader.readAsText(chunk); 335 } 336 else { 337 reader.readAsArrayBuffer(chunk); 338 } 339 }; 340 341 // Callback for when the reader has completed a load event. 342 var loadCb = function (event) { 343 // Abort if we are told to do so. 344 if (communicator.abort) return; 345 346 // Transmit the newly loaded data to the server and emit a client event 347 var bytesLoaded = Math.min(offset+chunkSize, file.size); 348 transmitPart(offset, bytesLoaded, event.target.result); 349 _dispatch("progress", { 350 file: file, 351 bytesLoaded: bytesLoaded, 352 name: newName 353 }); 354 355 // Get ready to send the next chunk 356 offset += chunkSize; 357 if (offset >= file.size) { 358 // All done! 359 transmitDone(); 360 _dispatch("load", { 361 file: file, 362 reader: reader, 363 name: newName 364 }); 365 uploadComplete = true; 366 } 367 }; 368 _listenTo(reader, "load", loadCb); 369 370 // Listen for an "error" event. Stop the transmission if one is received. 371 _listenTo(reader, "error", function () { 372 socket.emit(_getTopicName("_done"), _wrapData({ 373 id: id, 374 interrupt: true 375 }, "done")); 376 _stopListeningTo(reader, "load", loadCb); 377 }); 378 379 // Do the same for the "abort" event. 380 _listenTo(reader, "abort", function () { 381 socket.emit(_getTopicName("_done"), _wrapData({ 382 id: id, 383 interrupt: true 384 }, "done")); 385 _stopListeningTo(reader, "load", loadCb); 386 }); 387 388 // Transmit the "start" message to the server. 389 socket.emit(_getTopicName("_start"), _wrapData({ 390 name: file.name, 391 mtime: file.lastModified, 392 meta: file.meta, 393 size: file.size, 394 encoding: useText ? "text" : "octet", 395 id: id 396 }, "start")); 397 398 // To avoid a race condition, we don't want to start transmitting to the 399 // server until the server says it is ready. 400 var readyCallback = function (_newName) { 401 newName = _newName; 402 processChunk(); 403 }; 404 var chunkCallback = function(){ 405 if ( !uploadComplete ) 406 processChunk(); 407 }; 408 readyCallbacks[id] = readyCallback; 409 chunkCallbacks[id] = chunkCallback; 410 411 return communicator; 412 }; 413 414 /** 415 * Private function to load the file into memory using the HTML5 FileReader object 416 * and then transmit that file through Socket.IO. 417 * 418 * @param {FileList} files An array of files 419 * @return {void} 420 */ 421 var _load = function (files) { 422 // Iterate through the array of files. 423 for (var i = 0; i < files.length; i++) { 424 // Evaluate each file in a closure, because we will need a new 425 // instance of FileReader for each file. 426 var communicator = _loadOne(files[i]); 427 communicators[communicator.id] = communicator; 428 } 429 }; 430 431 /** 432 * Private function to fetch an HTMLInputElement instance that can be used 433 * during the file selection process. 434 * @return {void} 435 */ 436 var _getInputElement = function () { 437 var inpt = document.getElementById(self.fileInputElementId); 438 if (!inpt) { 439 inpt = document.createElement("input"); 440 inpt.setAttribute("type", "file"); 441 inpt.setAttribute("id", self.fileInputElementId); 442 inpt.style.display = "none"; 443 document.body.appendChild(inpt); 444 } 445 return inpt; 446 }; 447 448 /** 449 * Private function to remove an HTMLInputElement created by this instance 450 * of SIOFU.
451 * 452 * @return {void} 453 */ 454 var _removeInputElement = function () { 455 var inpt = document.getElementById(self.fileInputElementId); 456 if (inpt) { 457 inpt.parentNode.removeChild(inpt); 458 } 459 }; 460 461 var _baseFileSelectCallback = function (files) { 462 if (files.length === 0) return; 463 464 // Ensure existence of meta property on each file 465 for (var i = 0; i < files.length; i++) { 466 if(!files[i].meta) files[i].meta = {}; 467 } 468 469 // Dispatch the "choose" event 470 var evntResult = _dispatch("choose", { 471 files: files 472 }); 473 474 // If the callback didn't return false, continue with the upload 475 if (evntResult) { 476 _load(files); 477 } 478 }; 479 480 /** 481 * Private function that serves as a callback on file input. 482 * @param {Event} event The file input change event 483 * @return {void} 484 */ 485 var _fileSelectCallback = function (event) { 486 var files = event.target.files || event.dataTransfer.files; 487 event.preventDefault(); 488 _baseFileSelectCallback(files); 489 490 if (self.resetFileInputs) { 491 try { 492 event.target.value = ""; //for IE11, latest Chrome/Firefox/Opera... 493 } catch(err) { 494 // ignore 495 } 496 if (event.target.value) { //for IE5 ~ IE10 497 var form = document.createElement("form"), 498 parentNode = event.target.parentNode, ref = event.target.nextSibling; 499 form.appendChild(event.target); 500 form.reset(); 501 parentNode.insertBefore(event.target, ref); 502 } 503 } 504 }; 505 506 507 /** 508 * Submit files at arbitrary time 509 * @param {FileList} files Files received form the input element. 510 * @return {void} 511 */ 512 this.submitFiles = function (files) { 513 if (files) { 514 _baseFileSelectCallback(files); 515 } 516 }; 517 518 /** 519 * Use a submitButton to upload files from the field given 520 * @param {HTMLInputElement} submitButton the button that the user has to 521 * click to start the upload 522 * @param {HTMLInputElement} input the field with the data to upload 523 * 524 * @return {void} 525 */ 526 this.listenOnSubmit = function (submitButton, input) { 527 if (!input.files) return; 528 _listenTo(submitButton, "click", function () { 529 _baseFileSelectCallback(input.files); 530 }, false); 531 }; 532 533 /** 534 * Use a submitButton to upload files from the field given 535 * @param {HTMLInputElement} submitButton the button that the user has to 536 * click to start the upload 537 * @param {Array} array an array of fields with the files to upload 538 * 539 * @return {void} 540 */ 541 this.listenOnArraySubmit = function (submitButton, array) { 542 for (var index in array) { 543 this.listenOnSubmit(submitButton, array[index]); 544 } 545 }; 546 547 /** 548 * Use a file input to activate this instance of the file uploader. 549 * @param {HTMLInputElement} inpt The input element (e.g., as returned by 550 * document.getElementById("yourId")) 551 * @return {void} 552 */ 553 this.listenOnInput = function (inpt) { 554 if (!inpt.files) return; 555 _listenTo(inpt, "change", _fileSelectCallback, false); 556 }; 557 558 /** 559 * Accept files dropped on an element and upload them using this instance 560 * of the file uploader. 561 * @param {HTMLELement} div Any HTML element. When the user drags a file 562 * or files onto this element, those files will 563 * be processed by the instance. 564 * @return {void} 565 */ 566 this.listenOnDrop = function (div) { 567 // We need to preventDefault on the dragover event in order for the 568 // drag-and-drop operation to work. 569 _listenTo(div, "dragover", function (event) { 570 event.preventDefault(); 571 }, false); 572 573 _listenTo(div, "drop", _fileSelectCallback); 574 }; 575 576 /** 577 * Display a dialog box for the user to select a file. The file will then 578 * be uploaded using this instance of SocketIOFileUpload. 579 * 580 * This method works in all current browsers except Firefox, though Opera 581 * requires that the input element be visible. 582 * 583 * @return {void} 584 */ 585 this.prompt = function () { 586 var inpt = _getInputElement(); 587 588 // Listen for the "change" event on the file input element. 589 _listenTo(inpt, "change", _fileSelectCallback, false); 590 591 // Fire a click event on the input element. Firefox does not allow 592 // programatic clicks on input elements, but the other browsers do. 593 // Note that Opera requires that the element be visible when "clicked". 594 var evnt = document.createEvent("MouseEvents"); 595 evnt.initMouseEvent("click", true, true, window, 596 0, 0, 0, 0, 0, false, false, false, false, 0, null); 597 inpt.dispatchEvent(evnt); 598 }; 599 600 /** 601 * Destroy an instance of Socket.IO file upload (i.e., unbind events and 602 * relieve memory). 603 * 604 * IMPORTANT: To finish the memory relief process, set all external 605 * references to this instance of SIOFU (including the reference used to 606 * call this destroy function) to null. 607 * 608 * @return {void} 609 */ 610 this.destroy = function () { 611 _stopListening(); 612 _removeInputElement(); 613 for (var id in communicators) { 614 if (communicators.hasOwnProperty(id)) { 615 communicators[id].abort = true; 616 } 617 } 618 callbacks = null, uploadedFiles = null, readyCallbacks = null, communicators = null; 619 }; 620 621 /** 622 * Registers an event listener. If the callback function returns false, 623 * the file uploader will stop uploading the current file. 624 * @param {string} eventName Type of event for which to listen. 625 * @param {Function} callback Listener function. Will be passed the 626 * event as an argument when the event occurs. 627 * @return {void} 628 */ 629 this.addEventListener = function (eventName, callback) { 630 if (!callbacks[eventName]) callbacks[eventName] = []; 631 callbacks[eventName].push(callback); 632 }; 633 634 /** 635 * Removes an event listener. 636 * @param {string} eventName Type of event. 637 * @param {Function} callback Listener function to remove. 638 * @return {boolean} true if callback removed; false otherwise 639 */ 640 this.removeEventListener = function (eventName, callback) { 641 if (!callbacks[eventName]) return false;
642 for (var i = 0; i < callbacks[eventName].length; i++) { 643 if (callbacks[eventName][i] === callback) { 644 callbacks[eventName].splice(i, 1); 645 return true; 646 } 647 } 648 return false; 649 }; 650 651 /** 652 * Dispatches an event into this instance's event model. 653 * @param {Event} evnt The event to dispatch. 654 * @return {boolean} false if any callback returned false; true otherwise 655 */ 656 this.dispatchEvent = function (evnt) { 657 var eventCallbacks = callbacks[evnt.type]; 658 if (!eventCallbacks) return true; 659 var retVal = true; 660 for (var i = 0; i < eventCallbacks.length; i++) { 661 var callbackResult = eventCallbacks[i](evnt); 662 if (callbackResult === false) { 663 retVal = false; 664 } 665 } 666 return retVal; 667 }; 668 669 // OTHER LIBRARIES 670 /* 671 * base64-arraybuffer 672 * https://github.com/niklasvh/base64-arraybuffer 673 * 674 * Copyright (c) 2012 Niklas von Hertzen 675 * Licensed under the MIT license. 676 * 677 * Adapted for SocketIOFileUpload. 678 */ 679 var _uint8ArrayToBase64 = function (bytes) { 680 var i, len = bytes.buffer.byteLength, base64 = "", 681 chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 682 683 for (i = 0; i < len; i += 3) { 684 base64 += chars[bytes[i] >> 2]; 685 base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; 686 base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; 687 base64 += chars[bytes[i + 2] & 63]; 688 } 689 690 if ((len % 3) === 2) { 691 base64 = base64.substring(0, base64.length - 1) + "="; 692 } 693 else if (len % 3 === 1) { 694 base64 = base64.substring(0, base64.length - 2) + "=="; 695 } 696 697 return base64; 698 }; 699 // END OTHER LIBRARIES 700 var _chunckCallback = function(data) { 701 if ( chunkCallbacks[data.id] ) 702 chunkCallbacks[data.id](); 703 }; 704 705 var _readyCallback = function (data) { 706 if (readyCallbacks[data.id]) 707 readyCallbacks[data.id](data.name); 708 }; 709 710 var _completCallback = function (data) { 711 if (uploadedFiles[data.id]) { 712 _dispatch("complete", { 713 file: uploadedFiles[data.id], 714 detail: data.detail, 715 success: data.success 716 }); 717 } 718 }; 719 720 var _errorCallback = function (data) { 721 if ( uploadedFiles[data.id] ) { 722 _dispatch("error", { 723 file: uploadedFiles[data.id], 724 message: data.message, 725 code: 0 726 }); 727 if (communicators) communicators[data.id].abort = true; 728 } 729 }; 730 731 // CONSTRUCTOR: Listen to the "complete", "ready", and "error" messages 732 // on the socket. 733 if (_isWrapDataWellConfigured() && self.wrapData) { 734 var mapActionToCallback = { 735 chunk: _chunckCallback, 736 ready: _readyCallback, 737 complete: _completCallback, 738 error: _errorCallback 739 }; 740 741 _listenTo(socket, _getTopicName(), function (message) { 742 if (typeof message !== "object") { 743 console.log("SocketIOFileUploadClient Error: You choose to wrap your data so the message from the server need to be an object"); // eslint-disable-line no-console 744 return; 745 } 746 var actionKey = self.wrapData.unwrapKey && typeof self.wrapData.unwrapKey.action === "string" ? self.wrapData.unwrapKey.action : "action"; 747 var messageKey = self.wrapData.unwrapKey && typeof self.wrapData.unwrapKey.message === "string" ? self.wrapData.unwrapKey.message : "message"; 748 749 var action = message[actionKey]; 750 var data = message[messageKey]; 751 if (!action || !data || !mapActionToCallback[action]) { 752 console.log("SocketIOFileUploadClient Error: You choose to wrap your data but the message from the server is wrong configured. Check the message and your wrapData option"); // eslint-disable-line no-console 753 return; 754 } 755 mapActionToCallback[action](data); 756 }); 757 } else { 758 _listenTo(socket, _getTopicName("_chunk"), _chunckCallback); 759 _listenTo(socket, _getTopicName("_ready"), _readyCallback); 760 _listenTo(socket, _getTopicName("_complete"), _completCallback); 761 _listenTo(socket, _getTopicName("_error"), _errorCallback); 762 } 763 764 if (this.exposePrivateFunction) { 765 this.chunckCallback = _chunckCallback; 766 this.readyCallback = _readyCallback; 767 this.completCallback = _completCallback; 768 this.errorCallback = _errorCallback; 769 } 770 }; 771}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.