7. Daemon Views

Closing the browser that displays a Lona view will result in the view getting stopped on the server as well. When two browsers open the same URL at the same time, two view instances get started. This emulates the behavior of most other web frameworks.

Lona views run entirely on the server, thread-safe, and self-contained with all state in them. When View.is_daemon is set to True, a view can run in the background and be synchronized and displayed in multiple browsers at the same time. That also makes long running views possible, where long running tasks can be started and later revisited.

Daemonized views are only available to the same user that started the view.

Note

Daemonized views are not meant to be used to create multi-user views. They are meant to create single-user views that are long running and/or should be visible in multiple browsers at the same time.

If you want to create multi-user views, use channels instead.

Short Running View

This example implements a short running view, implementing a simple counter and a text area. By deamonizing it, its content gets synchronized between all browser tabs of the same user.

This style of view is called "short running" because its handle_request() method returns immediately.

Note

Daemonized views get removed immediately from the server when their LonaView.handle_request() returns. Since version 1.11, you can disable this and keep the daemonized view running by setting LonaView.STOP_DAEMON_WHEN_VIEW_FINISHES to False.

This behavior needs to be manually enabled due to backward compatibility, but will be the default from Lona 2.0 onwards.

from lona_picocss.html import InlineButton, TextArea, Span, HTML, Div, H1, Br
from lona_picocss import install_picocss

from lona import View, App

app = App(__file__)

install_picocss(app, debug=True)


@app.route('/')
class Index(View):
    STOP_DAEMON_WHEN_VIEW_FINISHES = False

    def handle_button_click(self, input_event):
        with self.html.lock:
            span = self.html.query_selector('span#counter')
            span.set_text(int(span.get_text()) + 1)

    def handle_request(self, request):

        # daemonize view
        self.is_daemon = True

        self.html = HTML(
            H1('Daemonized View'),
            Div(
                'Counter: ',
                Span('10', _id='counter'),
                ' ',
                InlineButton(
                    'Increment',
                    handle_click=self.handle_button_click,
                ),
                Br(),
                Br(),
            ),
            TextArea(placeholder='TextArea')
        )

        return self.html


if __name__ == '__main__':
    app.run()

Long Running View

This example implements a more complex counter, that counts on its own in the background and can be canceled using a pop-up. This style of view is called "long running" because its handle_request() method runs as long as the view runs.

This is handy for writing wizards or processing huge amounts of data.

Note

This style of view blocks one Python thread for its entire runtime.

from lona_picocss.html import (
    InlineButton,
    NumberInput,
    Progress,
    Modal,
    Label,
    HTML,
    Grid,
    Pre,
    H3,
    H1,
    P,
)
from lona_picocss import install_picocss

from lona import View, App

app = App(__file__)

install_picocss(app, debug=True)


@app.route('/')
class Index(View):
    def open_modal(self, input_event):
        with self.html.lock:
            self.modal.get_body().nodes = [
                H3('Stop Counter'),
                P('Are you sure you want to stop the counter?'),
            ]

            self.modal.get_footer().nodes = [
                InlineButton(
                    'Cancel',
                    secondary=True,
                    handle_click=lambda i: self.modal.close(),
                ),
                InlineButton(
                    'Stop',
                    handle_click=self.stop_counter,
                ),
            ]

            self.modal.open()

    def stop_counter(self, input_event):
        self.modal.close()
        self.running = False

    def handle_request(self, request):

        # daemonize view
        self.is_daemon = True

        # wait for start
        self.limit = NumberInput(value=10)

        self.html = HTML(
            H1('Counter'),
            Grid(
                Label(
                    'Limit',
                    self.limit,
                ),
                InlineButton('Start'),
            ),
        )

        self.await_click(html=self.html)

        # count to limit
        self.running = True
        self.modal = Modal()

        progress = Progress(value=0)

        pre = Pre(
            style={
                'height': f'{int(self.limit.value) + 4}em',
            },
        )

        self.html = HTML(
            H1('Counter'),
            progress,
            pre,
            InlineButton(
                'Stop',
                handle_click=self.open_modal,
                style={
                    'float': 'right',
                },
            ),
            self.modal,
        )

        for i in range(1, int(self.limit.value) + 1):

            # check if view should stop
            if not self.running:
                pre.write_line('view stopped')
                self.show(self.html)

                return

            progress.value = 100 / int(self.limit.value) * i
            pre.write_line(f'Counting to {i}')

            self.show(self.html)
            self.sleep(1)

        # finish
        pre.write_line('finish')

        return self.html


if __name__ == '__main__':
    app.run()