phoenix socket issue

I read the phoenix socket docs where they show how to share a websocket connection with channels & liveviews together. I did the same thing & it worked. The problem is- its synchronous. Like if im using the websocket connection for channels, its not time splicing or something. Its ensuring the task in the channel/ socket.js is complete after which liveview navigations is possible. I want this to be async. As in- run both of them together, the way a computer tasks concurrently. Is there a way to do this while sharing the same websocket connection? I dont really want two connections per client. Any ideas?

We need to know more about what you are doing. The websocket transport multiplexes the channels (and liveviews) on the concrete websocket connection, so blocking work in one channel won’t block work in another. They only share the line of communication so you would only block the client or server a channel was saturating the websocket, which is unlikely. Can you explain more about your goals and what you are doing currently? If you want to do blocking work inside the LiveView and allow navigation to continue, you can do that work in a supervised task elsewhere, but we need to see some example code and know more about what you’re doing. Thanks!

im so sorry to disturb you like this, im kindoff a noob really.
But what im trying to do is upload files to a form. Problem is, during upload, if a user decides to navigate, he cant since lv exits upon navigation to another lv. So I want something like background file uploads. (completely async)
So im sharing a websocket connection with a phoenix channel as well as with multiple liveviews. the channel is responsible for uploads. Im using the FileReader API to send the uploads to the channel. Once its sent to the channel, I can navigate between lv’s with ease, but while the js is in the process of sending it to the channel, im kinda stuck on the same page, so user_socket.js → user_channel.ex is blocking when the uploads are being transferred. & I dont want to have 2 websocket connections per user or anything like that, I want concurrency between the two tasks of uploading the file & having lv do its thing. So im kindoff reinventing the wheel with uploads etc. here which is a bit of a pain. Hence I wondered if theres a way to upload files in the background. Im not even chunking the file, directly getting the entire binaries, so theres that too.

heres the code- incomplete, just a prototype for now- user_socket.js

// Bring in Phoenix channels client library:
import {Socket} from "phoenix"

let socket = new Socket("/live")

socket.connect()

let channel = socket.channel("uploads", {})

let fileInput         = document.getElementById("file-input")
let save              = document.querySelector("#save") 
const filePreview = document.getElementById('imagePreview');
let content = document.querySelector("#content")
var reader = new FileReader();
fileInput.onchange = function() {isokig()}
filePreview.innerHTML = '';

function isokig() {
  console.log("check it out")
  if (fileInput.files && fileInput.files.length > 0) {
      for (const file of fileInput.files) {
            const previewContainer = document.createElement('div');
            
            const previewImage = document.createElement('img');
            previewImage.src = URL.createObjectURL(file);
            previewImage.style.maxWidth = '250px'; // Adjust the image size
            
            const cancelButton = document.createElement('button');
            cancelButton.textContent = 'Cancel';
            cancelButton.addEventListener('click', function() {
                filePreview.removeChild(previewContainer);
                removeFileFromInput(fileInput, file);
            });
            
            previewContainer.appendChild(previewImage);
            previewContainer.appendChild(cancelButton);
            filePreview.appendChild(previewContainer);
       };

    reader.readAsBinaryString(fileInput.files[0])
  }
};
  
save.onclick = function(event) {
    console.log(reader.result);
    channel.push("upload", {files: reader.result})
 };

function formatBytes(bytes, decimals = 2) {
    if (bytes === 0) return '0 Bytes';
    const k = 1024;
    const dm = decimals < 0 ? 0 : decimals;
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}

function removeFileFromInput(inputElement, index) {
    const newFiles = [];
    for (let i = 0; i < inputElement.files.length; i++) {
        if (inputElement.files[i] !== fileToRemove) {
            newFiles.push(inputElement.files[i]);
        }
    }
    inputElement.files = newFiles;
}

channel.join()
  .receive("ok", resp => { console.log("Joined successfully", resp) })
  .receive("error", resp => { console.log("Unable to join", resp) })

export default socket

Please let me know if anything else is required

You can use a sticky LV to have a LV persist across navigation (including uploads):

https://hexdocs.pm/phoenix_live_view/0.19.5/Phoenix.Component.html#live_render/2

Not reinventing uploads is certainly a good start, given teh complexities involved. You’d want to chunk uploads as well as make sure to use a binary channel to not incur overhead from encoding in text formats.

well my first attempt was sticky liveviews but trust me, thats not a very good solution. There are tons of UI issues associated with it. It will glitch in some cases & involves some not so great, CSS trickery which is difficult to get right, else the upload button just hovers atop the liveview page.
About chunking- yes id like to chunk uploads, but I cant apparently due to FileReader API sending it all at once as binaries. Problem is- it includes other data as well, hence just writing those binaries wont help you view the image (im trying to fix that right now), but {:binary, chunk} option in channels doesnt work here because it just sends the data as binary, & we can straightaway get it. So i dont really know how to chunk it yet, but probably the API will fix that, still, sticky liveviews is not really workable ig

Sounds like solving the css issues would be the way forward instead of trying to reinvent the whole upload handing. I’ve had no problems with sticky LVs and uploads when working with that recently.

1 Like

oh, is that so? So im assuming youre assigning a boolean in the socket on whether or not to display the sticky liveview upload option. & if yes, then display the upload files button. Is it possible if you can share the code?

I’m not assigning anything. There’s nothing to assign to a nested liveview. You can push data through the session, but changing that will likely remount the LV and render the sticky attribute useless.

youre pushing data through the session? im not quite sure what do you mean by that. sessions are limited to a certain size beyond which they crash, & definitely not large enough to upload photos in?
Can you share some code if possible? I want to see, the css + html as well

That’s what I setup about an hour ago. Drop a large file and navigate to your hearts contend.

I’m talking about the session option on live_render. Which you probably shouldn’t touch (or at least not change between pages) for this usecase.

thanks for this, ill check it out & get back. thanks again

ok ive taken a look at this, but have you actually run this with heex templates before? the result is- both the templates overlap or are like nested templates. To avoid that I did layout: false, but then the live_file_preview wasnt working. Hmmmm, ill have to take a better look at this

I’ve run this exact code as is, yes. You certainly want to use layout: false for the nested LV (no matter if sticky or not), as you don’t want to duplicate the layout part.

i did exactly what you did & as mentioned in the docs but for some reason I cant preview my image. Any ideas?

Oh wait, I spoke too soon, sorry, ignore. Its working for now, ill try to play around some more & update you on the progress. thanks for your help

ive done it, now 1 problem ive ran into is- how do I access the other lv from the current one? Basically I have just 1 submit button in the main liveview but I need to trigger the form submit event of the sticky liveview that holds my uploads from my main liveview using this button. What do I do?

You can target a form by id, instead of using nested html.
<button type="submit" form="#upload-form-id">Submit</button>

im not really nesting htmls it just inserts itself in with sticky lvs. But this is the 1st time im looking at such a syntax. form within a button? this button needs to submit the main lvs form as well as the sticky liveview form as well…