lively 0.4.0 → 0.6.0
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/bin/lively +16 -0
- data/lib/lively/application.rb +29 -5
- data/lib/lively/environment/application.rb +6 -1
- data/lib/lively/hello_world.rb +59 -0
- data/lib/lively/version.rb +1 -1
- data/public/_components/@socketry/live/Live.js +125 -75
- data/public/_components/@socketry/live/package.json +1 -1
- data/public/_components/@socketry/live/test/Live.js +108 -105
- data/public/_static/Falcon.png +0 -0
- data/public/_static/index.css +5 -1
- data/public/_static/site.css +1 -1
- data.tar.gz.sig +0 -0
- metadata +9 -5
- metadata.gz.sig +4 -4
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 5792cd3cb017762c79c0c8efcf99724db21dd444aa0e6e1f0c7f4f8e0e61774a
         | 
| 4 | 
            +
              data.tar.gz: 0275c27630c5e0af1408771737e9fa87276ce3b6e2f06de788eaa9e45d60a6a5
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 3c0f91c91d6d25d138aa78332cfb9c51d4f5793f5134c059bed8d1b6d1dd4b327a70f3d8e47106a49965534157d58913952b60f7508e903b461d3e15d48381bb
         | 
| 7 | 
            +
              data.tar.gz: c4b7817e61109d157bb68c157e8c50590776db6c1eca1c4d7f65936339ba2d02da62f07dfc903cd939d8807231dc382e6b7f18702ef0096266b3bd458b25f4cd
         | 
    
        checksums.yaml.gz.sig
    CHANGED
    
    | Binary file | 
    
        data/bin/lively
    ADDED
    
    | @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'async/service'
         | 
| 4 | 
            +
            require_relative '../lib/lively/environment/application'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            ARGV.each do |path|
         | 
| 7 | 
            +
            	require(path)
         | 
| 8 | 
            +
            end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            configuration = Async::Service::Configuration.build do
         | 
| 11 | 
            +
            	service "lively" do
         | 
| 12 | 
            +
            		include Lively::Environment::Application
         | 
| 13 | 
            +
            	end	
         | 
| 14 | 
            +
            end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            Async::Service::Controller.run(configuration)
         | 
    
        data/lib/lively/application.rb
    CHANGED
    
    | @@ -4,12 +4,32 @@ | |
| 4 4 | 
             
            # Copyright, 2021-2024, by Samuel Williams.
         | 
| 5 5 |  | 
| 6 6 | 
             
            require 'live'
         | 
| 7 | 
            +
            require 'protocol/http/middleware'
         | 
| 7 8 | 
             
            require 'async/websocket/adapters/http'
         | 
| 8 9 |  | 
| 9 10 | 
             
            require_relative 'pages/index'
         | 
| 11 | 
            +
            require_relative 'hello_world'
         | 
| 10 12 |  | 
| 11 13 | 
             
            module Lively
         | 
| 12 14 | 
             
            	class Application < Protocol::HTTP::Middleware
         | 
| 15 | 
            +
            		def self.[](tag)
         | 
| 16 | 
            +
            			klass = Class.new(self)
         | 
| 17 | 
            +
            			
         | 
| 18 | 
            +
            			klass.define_singleton_method(:resolver) do
         | 
| 19 | 
            +
            				Live::Resolver.allow(tag)
         | 
| 20 | 
            +
            			end
         | 
| 21 | 
            +
            			
         | 
| 22 | 
            +
            			klass.define_method(:body) do
         | 
| 23 | 
            +
            				tag.new
         | 
| 24 | 
            +
            			end
         | 
| 25 | 
            +
            			
         | 
| 26 | 
            +
            			return klass
         | 
| 27 | 
            +
            		end
         | 
| 28 | 
            +
            		
         | 
| 29 | 
            +
            		def self.resolver
         | 
| 30 | 
            +
            			Live::Resolver.allow(HelloWorld)
         | 
| 31 | 
            +
            		end
         | 
| 32 | 
            +
            		
         | 
| 13 33 | 
             
            		def initialize(delegate, resolver: self.class.resolver)
         | 
| 14 34 | 
             
            			super(delegate)
         | 
| 15 35 |  | 
| @@ -24,19 +44,23 @@ module Lively | |
| 24 44 | 
             
            			self.class.name
         | 
| 25 45 | 
             
            		end
         | 
| 26 46 |  | 
| 27 | 
            -
            		def body
         | 
| 28 | 
            -
            			 | 
| 47 | 
            +
            		def body(...)
         | 
| 48 | 
            +
            			HelloWorld.new(...)
         | 
| 49 | 
            +
            		end
         | 
| 50 | 
            +
            		
         | 
| 51 | 
            +
            		def index(...)
         | 
| 52 | 
            +
            			Pages::Index.new(title: self.title, body: self.body(...))
         | 
| 29 53 | 
             
            		end
         | 
| 30 54 |  | 
| 31 | 
            -
            		def  | 
| 32 | 
            -
            			 | 
| 55 | 
            +
            		def handle(request, ...)
         | 
| 56 | 
            +
            			return Protocol::HTTP::Response[200, [], [self.index(...).call]]
         | 
| 33 57 | 
             
            		end
         | 
| 34 58 |  | 
| 35 59 | 
             
            		def call(request)
         | 
| 36 60 | 
             
            			if request.path == '/live'
         | 
| 37 61 | 
             
            				return Async::WebSocket::Adapters::HTTP.open(request, &self.method(:live)) || Protocol::HTTP::Response[400]
         | 
| 38 62 | 
             
            			else
         | 
| 39 | 
            -
            				return  | 
| 63 | 
            +
            				return handle(request)
         | 
| 40 64 | 
             
            			end
         | 
| 41 65 | 
             
            		end
         | 
| 42 66 | 
             
            	end
         | 
| @@ -3,13 +3,18 @@ | |
| 3 3 | 
             
            # Released under the MIT License.
         | 
| 4 4 | 
             
            # Copyright, 2021-2024, by Samuel Williams.
         | 
| 5 5 |  | 
| 6 | 
            +
            require_relative '../application'
         | 
| 7 | 
            +
            require_relative '../assets'
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            require 'falcon/environment/server'
         | 
| 10 | 
            +
             | 
| 6 11 | 
             
            module Lively
         | 
| 7 12 | 
             
            	module Environment
         | 
| 8 13 | 
             
            		module Application
         | 
| 9 14 | 
             
            			include Falcon::Environment::Server
         | 
| 10 15 |  | 
| 11 16 | 
             
            			def application
         | 
| 12 | 
            -
            				if Object.const_defined?(:Application | 
| 17 | 
            +
            				if Object.const_defined?(:Application)
         | 
| 13 18 | 
             
            					::Application
         | 
| 14 19 | 
             
            				else
         | 
| 15 20 | 
             
            					Console.warn(self, "No Application class defined, using default.")
         | 
| @@ -0,0 +1,59 @@ | |
| 1 | 
            +
            module Lively
         | 
| 2 | 
            +
            	class HelloWorld < Live::View
         | 
| 3 | 
            +
            		def initialize(...)
         | 
| 4 | 
            +
            			super
         | 
| 5 | 
            +
            			
         | 
| 6 | 
            +
            			@clock = nil
         | 
| 7 | 
            +
            		end
         | 
| 8 | 
            +
            		
         | 
| 9 | 
            +
            		def bind(page)
         | 
| 10 | 
            +
            			super
         | 
| 11 | 
            +
            			
         | 
| 12 | 
            +
            			@clock ||= Async do
         | 
| 13 | 
            +
            				while true
         | 
| 14 | 
            +
            					self.update!
         | 
| 15 | 
            +
            					
         | 
| 16 | 
            +
            					sleep 1
         | 
| 17 | 
            +
            				end
         | 
| 18 | 
            +
            			end
         | 
| 19 | 
            +
            		end
         | 
| 20 | 
            +
            		
         | 
| 21 | 
            +
            		def close
         | 
| 22 | 
            +
            			@clock&.stop
         | 
| 23 | 
            +
            			
         | 
| 24 | 
            +
            			super
         | 
| 25 | 
            +
            		end
         | 
| 26 | 
            +
            		
         | 
| 27 | 
            +
            		def render(builder)
         | 
| 28 | 
            +
            			builder.tag(:h1) do
         | 
| 29 | 
            +
            				builder.text("Hello, I'm Lively!")
         | 
| 30 | 
            +
            			end
         | 
| 31 | 
            +
            			
         | 
| 32 | 
            +
            			builder.tag(:p) do
         | 
| 33 | 
            +
            				builder.text("The time is #{Time.now}.")
         | 
| 34 | 
            +
            			end
         | 
| 35 | 
            +
            			
         | 
| 36 | 
            +
            			builder.tag(:p) do
         | 
| 37 | 
            +
            				builder.text(<<~TEXT)
         | 
| 38 | 
            +
            					Lively is a simple client-server SPA framework. It is designed to be easy to use and understand, while providing a solid foundation for building interactive web applications. Create an `application.rb` file and define your own `Application` class to get started.
         | 
| 39 | 
            +
            				TEXT
         | 
| 40 | 
            +
            			end
         | 
| 41 | 
            +
            			
         | 
| 42 | 
            +
            			builder.inline_tag(:pre) do
         | 
| 43 | 
            +
            				builder.text(<<~TEXT)
         | 
| 44 | 
            +
            					#!/usr/bin/env lively
         | 
| 45 | 
            +
            					
         | 
| 46 | 
            +
            					class Application < Lively::Application
         | 
| 47 | 
            +
            						def body
         | 
| 48 | 
            +
            							Lively::HelloWorld.new
         | 
| 49 | 
            +
            						end
         | 
| 50 | 
            +
            					end
         | 
| 51 | 
            +
            				TEXT
         | 
| 52 | 
            +
            			end
         | 
| 53 | 
            +
            			
         | 
| 54 | 
            +
            			builder.tag(:p) do
         | 
| 55 | 
            +
            				builder.text("Check the `examples/` directory for... you guessed it... more examples.")
         | 
| 56 | 
            +
            			end
         | 
| 57 | 
            +
            		end
         | 
| 58 | 
            +
            	end
         | 
| 59 | 
            +
            end
         | 
    
        data/lib/lively/version.rb
    CHANGED
    
    
| @@ -1,6 +1,13 @@ | |
| 1 1 | 
             
            import morphdom from 'morphdom';
         | 
| 2 2 |  | 
| 3 3 | 
             
            export class Live {
         | 
| 4 | 
            +
            	#window;
         | 
| 5 | 
            +
            	#document;
         | 
| 6 | 
            +
            	#server;
         | 
| 7 | 
            +
            	#events;
         | 
| 8 | 
            +
            	#failures;
         | 
| 9 | 
            +
            	#reconnectTimer;
         | 
| 10 | 
            +
            	
         | 
| 4 11 | 
             
            	static start(options = {}) {
         | 
| 5 12 | 
             
            		let window = options.window || globalThis;
         | 
| 6 13 | 
             
            		let path = options.path || 'live'
         | 
| @@ -13,210 +20,253 @@ export class Live { | |
| 13 20 | 
             
            	}
         | 
| 14 21 |  | 
| 15 22 | 
             
            	constructor(window, url) {
         | 
| 16 | 
            -
            		this | 
| 17 | 
            -
            		this | 
| 23 | 
            +
            		this.#window = window;
         | 
| 24 | 
            +
            		this.#document = window.document;
         | 
| 18 25 |  | 
| 19 26 | 
             
            		this.url = url;
         | 
| 20 | 
            -
            		this | 
| 27 | 
            +
            		this.#server = null;
         | 
| 28 | 
            +
            		this.#events = [];
         | 
| 21 29 |  | 
| 22 | 
            -
            		this | 
| 30 | 
            +
            		this.#failures = 0;
         | 
| 31 | 
            +
            		this.#reconnectTimer = null;
         | 
| 23 32 |  | 
| 24 33 | 
             
            		// Track visibility state and connect if required:
         | 
| 25 | 
            -
            		this | 
| 26 | 
            -
            		 | 
| 34 | 
            +
            		this.#document.addEventListener("visibilitychange", () => this.#handleVisibilityChange());
         | 
| 35 | 
            +
            		
         | 
| 36 | 
            +
            		this.#handleVisibilityChange();
         | 
| 37 | 
            +
            		
         | 
| 38 | 
            +
            		const elementNodeType = this.#window.Node.ELEMENT_NODE;
         | 
| 27 39 |  | 
| 28 40 | 
             
            		// Create a MutationObserver to watch for removed nodes
         | 
| 29 | 
            -
            		this.observer = new this | 
| 41 | 
            +
            		this.observer = new this.#window.MutationObserver((mutationsList, observer) => {
         | 
| 30 42 | 
             
            			for (let mutation of mutationsList) {
         | 
| 31 43 | 
             
            				if (mutation.type === 'childList') {
         | 
| 32 44 | 
             
            					for (let node of mutation.removedNodes) {
         | 
| 45 | 
            +
            						if (node.nodeType !== elementNodeType) continue;
         | 
| 46 | 
            +
            						
         | 
| 33 47 | 
             
            						if (node.classList?.contains('live')) {
         | 
| 34 | 
            -
            							this | 
| 48 | 
            +
            							this.#unbind(node);
         | 
| 35 49 | 
             
            						}
         | 
| 36 50 |  | 
| 37 51 | 
             
            						// Unbind any child nodes:
         | 
| 38 52 | 
             
            						for (let child of node.getElementsByClassName('live')) {
         | 
| 39 | 
            -
            							this | 
| 53 | 
            +
            							this.#unbind(child);
         | 
| 40 54 | 
             
            						}
         | 
| 41 55 | 
             
            					}
         | 
| 42 56 |  | 
| 43 57 | 
             
            					for (let node of mutation.addedNodes) {
         | 
| 44 | 
            -
            						if (node. | 
| 45 | 
            -
             | 
| 58 | 
            +
            						if (node.nodeType !== elementNodeType) continue;
         | 
| 59 | 
            +
            						
         | 
| 60 | 
            +
            						if (node.classList.contains('live')) {
         | 
| 61 | 
            +
            							this.#bind(node);
         | 
| 46 62 | 
             
            						}
         | 
| 47 63 |  | 
| 48 64 | 
             
            						// Bind any child nodes:
         | 
| 49 65 | 
             
            						for (let child of node.getElementsByClassName('live')) {
         | 
| 50 | 
            -
            							this | 
| 66 | 
            +
            							this.#bind(child);
         | 
| 51 67 | 
             
            						}
         | 
| 52 68 | 
             
            					}
         | 
| 53 69 | 
             
            				}
         | 
| 54 70 | 
             
            			}
         | 
| 55 71 | 
             
            		});
         | 
| 56 72 |  | 
| 57 | 
            -
            		this.observer.observe(this | 
| 58 | 
            -
            		
         | 
| 59 | 
            -
            		this.attach();
         | 
| 73 | 
            +
            		this.observer.observe(this.#document.body, {childList: true, subtree: true});
         | 
| 60 74 | 
             
            	}
         | 
| 61 75 |  | 
| 62 76 | 
             
            	// -- Connection Handling --
         | 
| 63 77 |  | 
| 64 78 | 
             
            	connect() {
         | 
| 65 | 
            -
            		if (this | 
| 79 | 
            +
            		if (this.#server) {
         | 
| 80 | 
            +
            			return this.#server;
         | 
| 81 | 
            +
            		}
         | 
| 66 82 |  | 
| 67 | 
            -
            		let server = this | 
| 83 | 
            +
            		let server = this.#server = new this.#window.WebSocket(this.url);
         | 
| 84 | 
            +
            		
         | 
| 85 | 
            +
            		if (this.#reconnectTimer) {
         | 
| 86 | 
            +
            			clearTimeout(this.#reconnectTimer);
         | 
| 87 | 
            +
            			this.#reconnectTimer = null;
         | 
| 88 | 
            +
            		}
         | 
| 68 89 |  | 
| 69 90 | 
             
            		server.onopen = () => {
         | 
| 70 | 
            -
            			this | 
| 71 | 
            -
            			this | 
| 91 | 
            +
            			this.#failures = 0;
         | 
| 92 | 
            +
            			this.#flush();
         | 
| 93 | 
            +
            			this.#attach();
         | 
| 72 94 | 
             
            		};
         | 
| 73 95 |  | 
| 74 96 | 
             
            		server.onmessage = (message) => {
         | 
| 75 | 
            -
            			const [name, ... | 
| 97 | 
            +
            			const [name, ...args] = JSON.parse(message.data);
         | 
| 76 98 |  | 
| 77 | 
            -
            			this[name](... | 
| 99 | 
            +
            			this[name](...args);
         | 
| 78 100 | 
             
            		};
         | 
| 79 101 |  | 
| 80 102 | 
             
            		// The remote end has disconnected:
         | 
| 81 103 | 
             
            		server.addEventListener('error', () => {
         | 
| 82 | 
            -
            			this | 
| 104 | 
            +
            			this.#failures += 1;
         | 
| 83 105 | 
             
            		});
         | 
| 84 106 |  | 
| 85 107 | 
             
            		server.addEventListener('close', () => {
         | 
| 86 | 
            -
            			// Explicit disconnect will clear `this | 
| 87 | 
            -
            			if (this | 
| 108 | 
            +
            			// Explicit disconnect will clear `this.#server`:
         | 
| 109 | 
            +
            			if (this.#server && !this.#reconnectTimer) {
         | 
| 88 110 | 
             
            				// We need a minimum delay otherwise this can end up immediately invoking the callback:
         | 
| 89 | 
            -
            				const delay = Math.max(100 * (this | 
| 90 | 
            -
            				setTimeout(() =>  | 
| 111 | 
            +
            				const delay = Math.max(100 * (this.#failures + 1) ** 2, 60000);
         | 
| 112 | 
            +
            				this.#reconnectTimer = setTimeout(() => {
         | 
| 113 | 
            +
            					this.#reconnectTimer = null;
         | 
| 114 | 
            +
            					this.connect();
         | 
| 115 | 
            +
            				}, delay);
         | 
| 91 116 | 
             
            			}
         | 
| 92 117 |  | 
| 93 | 
            -
            			this | 
| 118 | 
            +
            			if (this.#server === server) {
         | 
| 119 | 
            +
            				this.#server = null;
         | 
| 120 | 
            +
            			}
         | 
| 94 121 | 
             
            		});
         | 
| 95 122 |  | 
| 96 123 | 
             
            		return server;
         | 
| 97 124 | 
             
            	}
         | 
| 98 125 |  | 
| 99 126 | 
             
            	disconnect() {
         | 
| 100 | 
            -
            		if (this | 
| 101 | 
            -
            			const server = this | 
| 102 | 
            -
            			this | 
| 127 | 
            +
            		if (this.#server) {
         | 
| 128 | 
            +
            			const server = this.#server;
         | 
| 129 | 
            +
            			this.#server = null;
         | 
| 103 130 | 
             
            			server.close();
         | 
| 104 131 | 
             
            		}
         | 
| 132 | 
            +
            		
         | 
| 133 | 
            +
            		if (this.#reconnectTimer) {
         | 
| 134 | 
            +
            			clearTimeout(this.#reconnectTimer);
         | 
| 135 | 
            +
            			this.#reconnectTimer = null;
         | 
| 136 | 
            +
            		}
         | 
| 105 137 | 
             
            	}
         | 
| 106 138 |  | 
| 107 | 
            -
            	send(message) {
         | 
| 108 | 
            -
            		if (this | 
| 139 | 
            +
            	#send(message) {
         | 
| 140 | 
            +
            		if (this.#server) {
         | 
| 109 141 | 
             
            			try {
         | 
| 110 | 
            -
            				return this | 
| 142 | 
            +
            				return this.#server.send(message);
         | 
| 111 143 | 
             
            			} catch (error) {
         | 
| 112 | 
            -
            				//  | 
| 144 | 
            +
            				// console.log("Live.send", "failed to send message to server", error);
         | 
| 113 145 | 
             
            			}
         | 
| 114 146 | 
             
            		}
         | 
| 115 147 |  | 
| 116 | 
            -
            		this | 
| 148 | 
            +
            		this.#events.push(message);
         | 
| 117 149 | 
             
            	}
         | 
| 118 150 |  | 
| 119 | 
            -
            	flush() {
         | 
| 120 | 
            -
            		if (this | 
| 151 | 
            +
            	#flush() {
         | 
| 152 | 
            +
            		if (this.#events.length === 0) return;
         | 
| 121 153 |  | 
| 122 | 
            -
            		let events = this | 
| 123 | 
            -
            		this | 
| 154 | 
            +
            		let events = this.#events;
         | 
| 155 | 
            +
            		this.#events = [];
         | 
| 124 156 |  | 
| 125 157 | 
             
            		for (var event of events) {
         | 
| 126 | 
            -
            			this | 
| 158 | 
            +
            			this.#send(event);
         | 
| 127 159 | 
             
            		}
         | 
| 128 160 | 
             
            	}
         | 
| 129 161 |  | 
| 130 | 
            -
            	handleVisibilityChange() {
         | 
| 131 | 
            -
            		if (this | 
| 162 | 
            +
            	#handleVisibilityChange() {
         | 
| 163 | 
            +
            		if (this.#document.hidden) {
         | 
| 132 164 | 
             
            			this.disconnect();
         | 
| 133 165 | 
             
            		} else {
         | 
| 134 166 | 
             
            			this.connect();
         | 
| 135 167 | 
             
            		}
         | 
| 136 168 | 
             
            	}
         | 
| 137 169 |  | 
| 138 | 
            -
            	bind(element) {
         | 
| 170 | 
            +
            	#bind(element) {
         | 
| 139 171 | 
             
            		console.log("bind", element.id, element.dataset);
         | 
| 140 172 |  | 
| 141 | 
            -
            		this | 
| 173 | 
            +
            		this.#send(JSON.stringify(['bind', element.id, element.dataset]));
         | 
| 142 174 | 
             
            	}
         | 
| 143 175 |  | 
| 144 | 
            -
            	unbind(element) {
         | 
| 176 | 
            +
            	#unbind(element) {
         | 
| 145 177 | 
             
            		console.log("unbind", element.id, element.dataset);
         | 
| 146 178 |  | 
| 147 | 
            -
            		this | 
| 179 | 
            +
            		if (this.#server) {
         | 
| 180 | 
            +
            			this.#send(JSON.stringify(['unbind', element.id]));
         | 
| 181 | 
            +
            		}
         | 
| 148 182 | 
             
            	}
         | 
| 149 183 |  | 
| 150 | 
            -
            	attach() {
         | 
| 151 | 
            -
            		for (let node of this | 
| 152 | 
            -
            			this | 
| 184 | 
            +
            	#attach() {
         | 
| 185 | 
            +
            		for (let node of this.#document.getElementsByClassName('live')) {
         | 
| 186 | 
            +
            			this.#bind(node);
         | 
| 153 187 | 
             
            		}
         | 
| 154 188 | 
             
            	}
         | 
| 155 189 |  | 
| 156 | 
            -
            	createDocumentFragment(html) {
         | 
| 157 | 
            -
            		return this | 
| 190 | 
            +
            	#createDocumentFragment(html) {
         | 
| 191 | 
            +
            		return this.#document.createRange().createContextualFragment(html);
         | 
| 158 192 | 
             
            	}
         | 
| 159 193 |  | 
| 160 | 
            -
            	reply(options) {
         | 
| 194 | 
            +
            	#reply(options, ...args) {
         | 
| 161 195 | 
             
            		if (options?.reply) {
         | 
| 162 | 
            -
            			this | 
| 196 | 
            +
            			this.#send(JSON.stringify(['reply', options.reply, ...args]));
         | 
| 163 197 | 
             
            		}
         | 
| 164 198 | 
             
            	}
         | 
| 165 199 |  | 
| 166 200 | 
             
            	// -- RPC Methods --
         | 
| 167 201 |  | 
| 202 | 
            +
            	script(id, code, options) {
         | 
| 203 | 
            +
            		let element = this.#document.getElementById(id);
         | 
| 204 | 
            +
            		
         | 
| 205 | 
            +
            		try {
         | 
| 206 | 
            +
            			let result = this.#window.Function(code).call(element);
         | 
| 207 | 
            +
            			
         | 
| 208 | 
            +
            			this.#reply(options, result);
         | 
| 209 | 
            +
            		} catch (error) {
         | 
| 210 | 
            +
            			this.#reply(options, null, {name: error.name, message: error.message, stack: error.stack});
         | 
| 211 | 
            +
            		}
         | 
| 212 | 
            +
            	}
         | 
| 213 | 
            +
            	
         | 
| 168 214 | 
             
            	update(id, html, options) {
         | 
| 169 | 
            -
            		let element = this | 
| 170 | 
            -
            		let fragment = this | 
| 215 | 
            +
            		let element = this.#document.getElementById(id);
         | 
| 216 | 
            +
            		let fragment = this.#createDocumentFragment(html);
         | 
| 171 217 |  | 
| 172 218 | 
             
            		morphdom(element, fragment);
         | 
| 173 219 |  | 
| 174 | 
            -
            		this | 
| 220 | 
            +
            		this.#reply(options);
         | 
| 175 221 | 
             
            	}
         | 
| 176 222 |  | 
| 177 223 | 
             
            	replace(selector, html, options) {
         | 
| 178 | 
            -
            		let elements = this | 
| 179 | 
            -
            		let fragment = this | 
| 224 | 
            +
            		let elements = this.#document.querySelectorAll(selector);
         | 
| 225 | 
            +
            		let fragment = this.#createDocumentFragment(html);
         | 
| 180 226 |  | 
| 181 227 | 
             
            		elements.forEach(element => morphdom(element, fragment.cloneNode(true)));
         | 
| 182 228 |  | 
| 183 | 
            -
            		this | 
| 229 | 
            +
            		this.#reply(options);
         | 
| 184 230 | 
             
            	}
         | 
| 185 231 |  | 
| 186 232 | 
             
            	prepend(selector, html, options) {
         | 
| 187 | 
            -
            		let elements = this | 
| 188 | 
            -
            		let fragment = this | 
| 233 | 
            +
            		let elements = this.#document.querySelectorAll(selector);
         | 
| 234 | 
            +
            		let fragment = this.#createDocumentFragment(html);
         | 
| 189 235 |  | 
| 190 236 | 
             
            		elements.forEach(element => element.prepend(fragment.cloneNode(true)));
         | 
| 191 237 |  | 
| 192 | 
            -
            		this | 
| 238 | 
            +
            		this.#reply(options);
         | 
| 193 239 | 
             
            	}
         | 
| 194 240 |  | 
| 195 241 | 
             
            	append(selector, html, options) {
         | 
| 196 | 
            -
            		let elements = this | 
| 197 | 
            -
            		let fragment = this | 
| 242 | 
            +
            		let elements = this.#document.querySelectorAll(selector);
         | 
| 243 | 
            +
            		let fragment = this.#createDocumentFragment(html);
         | 
| 198 244 |  | 
| 199 245 | 
             
            		elements.forEach(element => element.append(fragment.cloneNode(true)));
         | 
| 200 246 |  | 
| 201 | 
            -
            		this | 
| 247 | 
            +
            		this.#reply(options);
         | 
| 202 248 | 
             
            	}
         | 
| 203 249 |  | 
| 204 250 | 
             
            	remove(selector, options) {
         | 
| 205 | 
            -
            		let elements = this | 
| 251 | 
            +
            		let elements = this.#document.querySelectorAll(selector);
         | 
| 206 252 |  | 
| 207 253 | 
             
            		elements.forEach(element => element.remove());
         | 
| 208 254 |  | 
| 209 | 
            -
            		this | 
| 255 | 
            +
            		this.#reply(options);
         | 
| 210 256 | 
             
            	}
         | 
| 211 257 |  | 
| 212 258 | 
             
            	dispatchEvent(selector, type, options) {
         | 
| 213 | 
            -
            		let elements = this | 
| 259 | 
            +
            		let elements = this.#document.querySelectorAll(selector);
         | 
| 214 260 |  | 
| 215 261 | 
             
            		elements.forEach(element => element.dispatchEvent(
         | 
| 216 | 
            -
            			new this | 
| 262 | 
            +
            			new this.#window.CustomEvent(type, options)
         | 
| 217 263 | 
             
            		));
         | 
| 218 264 |  | 
| 219 | 
            -
            		this | 
| 265 | 
            +
            		this.#reply(options);
         | 
| 266 | 
            +
            	}
         | 
| 267 | 
            +
            	
         | 
| 268 | 
            +
            	error(message) {
         | 
| 269 | 
            +
            		console.error("Live.error", ...arguments);
         | 
| 220 270 | 
             
            	}
         | 
| 221 271 |  | 
| 222 272 | 
             
            	// -- Event Handling --
         | 
| @@ -224,19 +274,19 @@ export class Live { | |
| 224 274 | 
             
            	forward(id, event) {
         | 
| 225 275 | 
             
            		this.connect();
         | 
| 226 276 |  | 
| 227 | 
            -
            		this | 
| 277 | 
            +
            		this.#send(
         | 
| 228 278 | 
             
            			JSON.stringify(['event', id, event])
         | 
| 229 279 | 
             
            		);
         | 
| 230 280 | 
             
            	}
         | 
| 231 281 |  | 
| 232 | 
            -
            	forwardEvent(id, event, detail) {
         | 
| 233 | 
            -
            		event.preventDefault();
         | 
| 282 | 
            +
            	forwardEvent(id, event, detail, preventDefault = false) {
         | 
| 283 | 
            +
            		if (preventDefault) event.preventDefault();
         | 
| 234 284 |  | 
| 235 285 | 
             
            		this.forward(id, {type: event.type, detail: detail});
         | 
| 236 286 | 
             
            	}
         | 
| 237 287 |  | 
| 238 | 
            -
            	forwardFormEvent(id, event, detail) {
         | 
| 239 | 
            -
            		event.preventDefault();
         | 
| 288 | 
            +
            	forwardFormEvent(id, event, detail, preventDefault = true) {
         | 
| 289 | 
            +
            		if (preventDefault) event.preventDefault();
         | 
| 240 290 |  | 
| 241 291 | 
             
            		let form = event.form;
         | 
| 242 292 | 
             
            		let formData = new FormData(form);
         | 
| @@ -1,13 +1,52 @@ | |
| 1 | 
            -
            import {describe, before, after, it} from 'node:test';
         | 
| 1 | 
            +
            import {describe, before, beforeEach, after, it} from 'node:test';
         | 
| 2 2 | 
             
            import {ok, strict, strictEqual, deepStrictEqual} from 'node:assert';
         | 
| 3 3 |  | 
| 4 4 | 
             
            import {WebSocket} from 'ws';
         | 
| 5 5 | 
             
            import {JSDOM} from 'jsdom';
         | 
| 6 6 | 
             
            import {Live} from '../Live.js';
         | 
| 7 7 |  | 
| 8 | 
            +
            class Queue {
         | 
| 9 | 
            +
            	constructor() {
         | 
| 10 | 
            +
            		this.items = [];
         | 
| 11 | 
            +
            		this.waiting = [];
         | 
| 12 | 
            +
            	}
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            	push(item) {
         | 
| 15 | 
            +
            		if (this.waiting.length > 0) {
         | 
| 16 | 
            +
            			let resolve = this.waiting.shift();
         | 
| 17 | 
            +
            			resolve(item);
         | 
| 18 | 
            +
            		} else {
         | 
| 19 | 
            +
            			this.items.push(item);
         | 
| 20 | 
            +
            		}
         | 
| 21 | 
            +
            	}
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            	pop() {
         | 
| 24 | 
            +
            		return new Promise(resolve => {
         | 
| 25 | 
            +
            			if (this.items.length > 0) {
         | 
| 26 | 
            +
            				resolve(this.items.shift());
         | 
| 27 | 
            +
            			} else {
         | 
| 28 | 
            +
            				this.waiting.push(resolve);
         | 
| 29 | 
            +
            			}
         | 
| 30 | 
            +
            		});
         | 
| 31 | 
            +
            	}
         | 
| 32 | 
            +
            	
         | 
| 33 | 
            +
            	async popUntil(callback) {
         | 
| 34 | 
            +
            		while (true) {
         | 
| 35 | 
            +
            			let item = await this.pop();
         | 
| 36 | 
            +
            			if (callback(item)) return item;
         | 
| 37 | 
            +
            		}
         | 
| 38 | 
            +
            	}
         | 
| 39 | 
            +
            	
         | 
| 40 | 
            +
            	clear() {
         | 
| 41 | 
            +
            		this.items = [];
         | 
| 42 | 
            +
            		this.waiting = [];
         | 
| 43 | 
            +
            	}
         | 
| 44 | 
            +
            }
         | 
| 45 | 
            +
             | 
| 8 46 | 
             
            describe('Live', function () {
         | 
| 9 47 | 
             
            	let dom;
         | 
| 10 48 | 
             
            	let webSocketServer;
         | 
| 49 | 
            +
            	let messages = new Queue();
         | 
| 11 50 |  | 
| 12 51 | 
             
            	const webSocketServerConfig = {port: 3000};
         | 
| 13 52 | 
             
            	const webSocketServerURL = `ws://localhost:${webSocketServerConfig.port}/live`;
         | 
| @@ -18,13 +57,24 @@ describe('Live', function () { | |
| 18 57 | 
             
            			webSocketServer.on('error', reject);
         | 
| 19 58 | 
             
            		});
         | 
| 20 59 |  | 
| 21 | 
            -
            		dom = new JSDOM('<!DOCTYPE html><html><body><div id="my"><p>Hello World</p></div></body></html>');
         | 
| 60 | 
            +
            		dom = new JSDOM('<!DOCTYPE html><html><body><div id="my" class="live"><p>Hello World</p></div></body></html>');
         | 
| 22 61 | 
             
            		// Ensure the WebSocket class is available:
         | 
| 23 62 | 
             
            		dom.window.WebSocket = WebSocket;
         | 
| 24 63 |  | 
| 25 64 | 
             
            		await new Promise(resolve => dom.window.addEventListener('load', resolve));
         | 
| 26 65 |  | 
| 27 66 | 
             
            		await listening;
         | 
| 67 | 
            +
            		
         | 
| 68 | 
            +
            		webSocketServer.on('connection', socket => {
         | 
| 69 | 
            +
            			socket.on('message', message => {
         | 
| 70 | 
            +
            				let payload = JSON.parse(message);
         | 
| 71 | 
            +
            				messages.push(payload);
         | 
| 72 | 
            +
            			});
         | 
| 73 | 
            +
            		});
         | 
| 74 | 
            +
            	});
         | 
| 75 | 
            +
            	
         | 
| 76 | 
            +
            	beforeEach(function () {
         | 
| 77 | 
            +
            		messages.clear();
         | 
| 28 78 | 
             
            	});
         | 
| 29 79 |  | 
| 30 80 | 
             
            	after(function () {
         | 
| @@ -35,9 +85,9 @@ describe('Live', function () { | |
| 35 85 | 
             
            		const live = Live.start({window: dom.window, base: 'http://localhost/'});
         | 
| 36 86 | 
             
            		ok(live);
         | 
| 37 87 |  | 
| 38 | 
            -
            		strictEqual(live.window, dom.window);
         | 
| 39 | 
            -
            		strictEqual(live.document, dom.window.document);
         | 
| 40 88 | 
             
            		strictEqual(live.url.href, 'ws://localhost/live');
         | 
| 89 | 
            +
            		
         | 
| 90 | 
            +
            		live.disconnect();
         | 
| 41 91 | 
             
            	});
         | 
| 42 92 |  | 
| 43 93 | 
             
            	it('should connect to the WebSocket server', function () {
         | 
| @@ -49,23 +99,51 @@ describe('Live', function () { | |
| 49 99 | 
             
            		live.disconnect();
         | 
| 50 100 | 
             
            	});
         | 
| 51 101 |  | 
| 52 | 
            -
            	it('should handle visibility changes', function () {
         | 
| 102 | 
            +
            	it('should handle visibility changes', async function () {
         | 
| 103 | 
            +
            		const live = new Live(dom.window, webSocketServerURL);
         | 
| 104 | 
            +
            		
         | 
| 105 | 
            +
            		// It's tricky to test the method directly.
         | 
| 106 | 
            +
            		// - Changing document.hidden is a hack.
         | 
| 107 | 
            +
            		// - Sending custom events seems to cause a hang.
         | 
| 108 | 
            +
            		
         | 
| 109 | 
            +
            		live.connect();
         | 
| 110 | 
            +
            		deepStrictEqual(await messages.pop(), ['bind', 'my', {}]);
         | 
| 111 | 
            +
            		
         | 
| 112 | 
            +
            		live.disconnect();
         | 
| 113 | 
            +
            		
         | 
| 114 | 
            +
            		live.connect()
         | 
| 115 | 
            +
            		deepStrictEqual(await messages.pop(), ['bind', 'my', {}]);
         | 
| 116 | 
            +
            		
         | 
| 117 | 
            +
            		live.disconnect();
         | 
| 118 | 
            +
            	});
         | 
| 119 | 
            +
            	
         | 
| 120 | 
            +
            	it('can execute scripts', async function () {
         | 
| 53 121 | 
             
            		const live = new Live(dom.window, webSocketServerURL);
         | 
| 54 122 |  | 
| 55 | 
            -
            		 | 
| 56 | 
            -
            		 | 
| 57 | 
            -
             | 
| 123 | 
            +
            		live.connect();
         | 
| 124 | 
            +
            		
         | 
| 125 | 
            +
            		const connected = new Promise(resolve => {
         | 
| 126 | 
            +
            			webSocketServer.on('connection', resolve);
         | 
| 58 127 | 
             
            		});
         | 
| 59 128 |  | 
| 60 | 
            -
            		 | 
| 129 | 
            +
            		let socket = await connected;
         | 
| 61 130 |  | 
| 62 | 
            -
            		 | 
| 131 | 
            +
            		socket.send(
         | 
| 132 | 
            +
            			JSON.stringify(['script', 'my', 'return 1+2', {reply: true}])
         | 
| 133 | 
            +
            		);
         | 
| 134 | 
            +
            		
         | 
| 135 | 
            +
            		let successReply = await messages.popUntil(message => message[0] == 'reply');
         | 
| 136 | 
            +
            		strictEqual(successReply[2], 3);
         | 
| 63 137 |  | 
| 64 | 
            -
            		 | 
| 138 | 
            +
            		socket.send(
         | 
| 139 | 
            +
            			JSON.stringify(['script', 'my', 'throw new Error("Test Error")', {reply: true}])
         | 
| 140 | 
            +
            		);
         | 
| 65 141 |  | 
| 66 | 
            -
            		 | 
| 142 | 
            +
            		let errorReply = await messages.popUntil(message => message[0] == 'reply');
         | 
| 143 | 
            +
            		strictEqual(errorReply[2], null);
         | 
| 144 | 
            +
            		console.log(errorReply);
         | 
| 67 145 |  | 
| 68 | 
            -
            		 | 
| 146 | 
            +
            		live.disconnect();
         | 
| 69 147 | 
             
            	});
         | 
| 70 148 |  | 
| 71 149 | 
             
            	it('should handle updates', async function () {
         | 
| @@ -79,18 +157,11 @@ describe('Live', function () { | |
| 79 157 |  | 
| 80 158 | 
             
            		let socket = await connected;
         | 
| 81 159 |  | 
| 82 | 
            -
            		const reply = new Promise((resolve, reject) => {
         | 
| 83 | 
            -
            			socket.on('message', message => {
         | 
| 84 | 
            -
            				let payload = JSON.parse(message);
         | 
| 85 | 
            -
            				if (payload[0] == 'reply') resolve(payload);
         | 
| 86 | 
            -
            			});
         | 
| 87 | 
            -
            		});
         | 
| 88 | 
            -
            		
         | 
| 89 160 | 
             
            		socket.send(
         | 
| 90 161 | 
             
            			JSON.stringify(['update', 'my', '<div id="my"><p>Goodbye World!</p></div>', {reply: true}])
         | 
| 91 162 | 
             
            		);
         | 
| 92 163 |  | 
| 93 | 
            -
            		await reply;
         | 
| 164 | 
            +
            		await messages.popUntil(message => message[0] == 'reply');
         | 
| 94 165 |  | 
| 95 166 | 
             
            		strictEqual(dom.window.document.getElementById('my').innerHTML, '<p>Goodbye World!</p>');
         | 
| 96 167 |  | 
| @@ -108,21 +179,11 @@ describe('Live', function () { | |
| 108 179 |  | 
| 109 180 | 
             
            		let socket = await connected;
         | 
| 110 181 |  | 
| 111 | 
            -
            		const reply = new Promise((resolve, reject) => {
         | 
| 112 | 
            -
            			socket.on('message', message => {
         | 
| 113 | 
            -
            				let payload = JSON.parse(message);
         | 
| 114 | 
            -
            				console.log("message", payload);
         | 
| 115 | 
            -
            				if (payload[0] == 'bind') resolve(payload);
         | 
| 116 | 
            -
            				else console.log("ignoring", payload);
         | 
| 117 | 
            -
            			});
         | 
| 118 | 
            -
            		});
         | 
| 119 | 
            -
            		
         | 
| 120 182 | 
             
            		socket.send(
         | 
| 121 183 | 
             
            			JSON.stringify(['update', 'my', '<div id="my"><div id="mychild" class="live"></div></div>'])
         | 
| 122 184 | 
             
            		);
         | 
| 123 185 |  | 
| 124 | 
            -
            		let payload = await  | 
| 125 | 
            -
            		
         | 
| 186 | 
            +
            		let payload = await messages.popUntil(message => message[0] == 'bind');
         | 
| 126 187 | 
             
            		deepStrictEqual(payload, ['bind', 'mychild', {}]);
         | 
| 127 188 |  | 
| 128 189 | 
             
            		live.disconnect();
         | 
| @@ -135,25 +196,11 @@ describe('Live', function () { | |
| 135 196 |  | 
| 136 197 | 
             
            		live.connect();
         | 
| 137 198 |  | 
| 138 | 
            -
            		const connected = new Promise(resolve => {
         | 
| 139 | 
            -
            			webSocketServer.on('connection', resolve);
         | 
| 140 | 
            -
            		});
         | 
| 141 | 
            -
            		
         | 
| 142 | 
            -
            		let socket = await connected;
         | 
| 143 | 
            -
            		
         | 
| 144 | 
            -
            		const reply = new Promise((resolve, reject) => {
         | 
| 145 | 
            -
            			socket.on('message', message => {
         | 
| 146 | 
            -
            				let payload = JSON.parse(message);
         | 
| 147 | 
            -
            				if (payload[0] == 'unbind') resolve(payload);
         | 
| 148 | 
            -
            				else console.log("ignoring", payload);
         | 
| 149 | 
            -
            			});
         | 
| 150 | 
            -
            		});
         | 
| 151 | 
            -
            		
         | 
| 152 | 
            -
            		live.attach();
         | 
| 153 | 
            -
            		
         | 
| 154 199 | 
             
            		dom.window.document.getElementById('my').remove();
         | 
| 155 200 |  | 
| 156 | 
            -
            		let payload = await  | 
| 201 | 
            +
            		let payload = await messages.popUntil(message => {
         | 
| 202 | 
            +
            			return message[0] == 'unbind' && message[1] == 'my';
         | 
| 203 | 
            +
            		});
         | 
| 157 204 |  | 
| 158 205 | 
             
            		deepStrictEqual(payload, ['unbind', 'my']);
         | 
| 159 206 |  | 
| @@ -173,20 +220,11 @@ describe('Live', function () { | |
| 173 220 |  | 
| 174 221 | 
             
            		let socket = await connected;
         | 
| 175 222 |  | 
| 176 | 
            -
            		const reply = new Promise((resolve, reject) => {
         | 
| 177 | 
            -
            			socket.on('message', message => {
         | 
| 178 | 
            -
            				let payload = JSON.parse(message);
         | 
| 179 | 
            -
            				if (payload[0] == 'reply') resolve(payload);
         | 
| 180 | 
            -
            				else console.log("ignoring", payload);
         | 
| 181 | 
            -
            			});
         | 
| 182 | 
            -
            		});
         | 
| 183 | 
            -
            		
         | 
| 184 223 | 
             
            		socket.send(
         | 
| 185 224 | 
             
            			JSON.stringify(['replace', '#my p', '<p>Replaced!</p>', {reply: true}])
         | 
| 186 225 | 
             
            		);
         | 
| 187 226 |  | 
| 188 | 
            -
            		await reply;
         | 
| 189 | 
            -
            		
         | 
| 227 | 
            +
            		await messages.popUntil(message => message[0] == 'reply');
         | 
| 190 228 | 
             
            		strictEqual(dom.window.document.getElementById('my').innerHTML, '<p>Replaced!</p>');
         | 
| 191 229 |  | 
| 192 230 | 
             
            		live.disconnect();
         | 
| @@ -207,20 +245,11 @@ describe('Live', function () { | |
| 207 245 | 
             
            			JSON.stringify(['update', 'my', '<div id="my"><p>Middle</p></div>'])
         | 
| 208 246 | 
             
            		);
         | 
| 209 247 |  | 
| 210 | 
            -
            		const reply = new Promise((resolve, reject) => {
         | 
| 211 | 
            -
            			socket.on('message', message => {
         | 
| 212 | 
            -
            				let payload = JSON.parse(message);
         | 
| 213 | 
            -
            				if (payload[0] == 'reply') resolve(payload);
         | 
| 214 | 
            -
            				else console.log("ignoring", payload);
         | 
| 215 | 
            -
            			});
         | 
| 216 | 
            -
            		});
         | 
| 217 | 
            -
            		
         | 
| 218 248 | 
             
            		socket.send(
         | 
| 219 249 | 
             
            			JSON.stringify(['prepend', '#my', '<p>Prepended!</p>', {reply: true}])
         | 
| 220 250 | 
             
            		);
         | 
| 221 251 |  | 
| 222 | 
            -
            		await reply;
         | 
| 223 | 
            -
            		
         | 
| 252 | 
            +
            		await messages.popUntil(message => message[0] == 'reply');
         | 
| 224 253 | 
             
            		strictEqual(dom.window.document.getElementById('my').innerHTML, '<p>Prepended!</p><p>Middle</p>');
         | 
| 225 254 |  | 
| 226 255 | 
             
            		live.disconnect();
         | 
| @@ -241,20 +270,11 @@ describe('Live', function () { | |
| 241 270 | 
             
            			JSON.stringify(['update', 'my', '<div id="my"><p>Middle</p></div>'])
         | 
| 242 271 | 
             
            		);
         | 
| 243 272 |  | 
| 244 | 
            -
            		const reply = new Promise((resolve, reject) => {
         | 
| 245 | 
            -
            			socket.on('message', message => {
         | 
| 246 | 
            -
            				let payload = JSON.parse(message);
         | 
| 247 | 
            -
            				if (payload[0] == 'reply') resolve(payload);
         | 
| 248 | 
            -
            				else console.log("ignoring", payload);
         | 
| 249 | 
            -
            			});
         | 
| 250 | 
            -
            		});
         | 
| 251 | 
            -
            		
         | 
| 252 273 | 
             
            		socket.send(
         | 
| 253 274 | 
             
            			JSON.stringify(['append', '#my', '<p>Appended!</p>', {reply: true}])
         | 
| 254 275 | 
             
            		);
         | 
| 255 276 |  | 
| 256 | 
            -
            		await reply;
         | 
| 257 | 
            -
            		
         | 
| 277 | 
            +
            		await messages.popUntil(message => message[0] == 'reply');
         | 
| 258 278 | 
             
            		strictEqual(dom.window.document.getElementById('my').innerHTML, '<p>Middle</p><p>Appended!</p>');
         | 
| 259 279 |  | 
| 260 280 | 
             
            		live.disconnect();
         | 
| @@ -275,19 +295,11 @@ describe('Live', function () { | |
| 275 295 | 
             
            			JSON.stringify(['update', 'my', '<div id="my"><p>Middle</p></div>'])
         | 
| 276 296 | 
             
            		);
         | 
| 277 297 |  | 
| 278 | 
            -
            		const reply = new Promise((resolve, reject) => {
         | 
| 279 | 
            -
            			socket.on('message', message => {
         | 
| 280 | 
            -
            				let payload = JSON.parse(message);
         | 
| 281 | 
            -
            				if (payload[0] == 'reply') resolve(payload);
         | 
| 282 | 
            -
            			});
         | 
| 283 | 
            -
            		});
         | 
| 284 | 
            -
            		
         | 
| 285 298 | 
             
            		socket.send(
         | 
| 286 299 | 
             
            			JSON.stringify(['remove', '#my p', {reply: true}])
         | 
| 287 300 | 
             
            		);
         | 
| 288 301 |  | 
| 289 | 
            -
            		await reply;
         | 
| 290 | 
            -
            		
         | 
| 302 | 
            +
            		await messages.popUntil(message => message[0] == 'reply');
         | 
| 291 303 | 
             
            		strictEqual(dom.window.document.getElementById('my').innerHTML, '');
         | 
| 292 304 |  | 
| 293 305 | 
             
            		live.disconnect();
         | 
| @@ -304,18 +316,11 @@ describe('Live', function () { | |
| 304 316 |  | 
| 305 317 | 
             
            		let socket = await connected;
         | 
| 306 318 |  | 
| 307 | 
            -
            		const reply = new Promise((resolve, reject) => {
         | 
| 308 | 
            -
            			socket.on('message', message => {
         | 
| 309 | 
            -
            				let payload = JSON.parse(message);
         | 
| 310 | 
            -
            				if (payload[0] == 'reply') resolve(payload);
         | 
| 311 | 
            -
            			});
         | 
| 312 | 
            -
            		});
         | 
| 313 | 
            -
            		
         | 
| 314 319 | 
             
            		socket.send(
         | 
| 315 320 | 
             
            			JSON.stringify(['dispatchEvent', '#my', 'click', {reply: true}])
         | 
| 316 321 | 
             
            		);
         | 
| 317 322 |  | 
| 318 | 
            -
            		await reply;
         | 
| 323 | 
            +
            		await messages.popUntil(message => message[0] == 'reply');
         | 
| 319 324 |  | 
| 320 325 | 
             
            		live.disconnect();
         | 
| 321 326 | 
             
            	});
         | 
| @@ -331,24 +336,22 @@ describe('Live', function () { | |
| 331 336 |  | 
| 332 337 | 
             
            		let socket = await connected;
         | 
| 333 338 |  | 
| 334 | 
            -
            		const reply = new Promise((resolve, reject) => {
         | 
| 335 | 
            -
            			socket.on('message', message => {
         | 
| 336 | 
            -
            				let payload = JSON.parse(message);
         | 
| 337 | 
            -
            				if (payload[0] == 'event') resolve(payload);
         | 
| 338 | 
            -
            			});
         | 
| 339 | 
            -
            		});
         | 
| 340 | 
            -
            		
         | 
| 341 339 | 
             
            		dom.window.document.getElementById('my').addEventListener('click', event => {
         | 
| 342 340 | 
             
            			live.forwardEvent('my', event);
         | 
| 343 341 | 
             
            		});
         | 
| 344 342 |  | 
| 345 343 | 
             
            		dom.window.document.getElementById('my').click();
         | 
| 346 344 |  | 
| 347 | 
            -
            		let payload = await  | 
| 348 | 
            -
            		
         | 
| 345 | 
            +
            		let payload = await messages.popUntil(message => message[0] == 'event');
         | 
| 349 346 | 
             
            		strictEqual(payload[1], 'my');
         | 
| 350 347 | 
             
            		strictEqual(payload[2].type, 'click');
         | 
| 351 348 |  | 
| 352 349 | 
             
            		live.disconnect();
         | 
| 353 350 | 
             
            	});
         | 
| 351 | 
            +
            	
         | 
| 352 | 
            +
            	it ('can log errors', function () {
         | 
| 353 | 
            +
            		const live = new Live(dom.window, webSocketServerURL);
         | 
| 354 | 
            +
            		
         | 
| 355 | 
            +
            		live.error('my', 'Test Error');
         | 
| 356 | 
            +
            	});
         | 
| 354 357 | 
             
            });
         | 
| Binary file | 
    
        data/public/_static/index.css
    CHANGED
    
    
    
        data/public/_static/site.css
    CHANGED
    
    
    
        data.tar.gz.sig
    CHANGED
    
    | Binary file | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: lively
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.6.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Samuel Williams
         | 
| @@ -37,7 +37,7 @@ cert_chain: | |
| 37 37 | 
             
              Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
         | 
| 38 38 | 
             
              voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
         | 
| 39 39 | 
             
              -----END CERTIFICATE-----
         | 
| 40 | 
            -
            date: 2024-05- | 
| 40 | 
            +
            date: 2024-05-06 00:00:00.000000000 Z
         | 
| 41 41 | 
             
            dependencies:
         | 
| 42 42 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 43 43 | 
             
              name: falcon
         | 
| @@ -59,14 +59,14 @@ dependencies: | |
| 59 59 | 
             
                requirements:
         | 
| 60 60 | 
             
                - - "~>"
         | 
| 61 61 | 
             
                  - !ruby/object:Gem::Version
         | 
| 62 | 
            -
                    version: '0. | 
| 62 | 
            +
                    version: '0.9'
         | 
| 63 63 | 
             
              type: :runtime
         | 
| 64 64 | 
             
              prerelease: false
         | 
| 65 65 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 66 66 | 
             
                requirements:
         | 
| 67 67 | 
             
                - - "~>"
         | 
| 68 68 | 
             
                  - !ruby/object:Gem::Version
         | 
| 69 | 
            -
                    version: '0. | 
| 69 | 
            +
                    version: '0.9'
         | 
| 70 70 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 71 71 | 
             
              name: xrb
         | 
| 72 72 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -83,14 +83,17 @@ dependencies: | |
| 83 83 | 
             
                    version: '0'
         | 
| 84 84 | 
             
            description:
         | 
| 85 85 | 
             
            email:
         | 
| 86 | 
            -
            executables: | 
| 86 | 
            +
            executables:
         | 
| 87 | 
            +
            - lively
         | 
| 87 88 | 
             
            extensions: []
         | 
| 88 89 | 
             
            extra_rdoc_files: []
         | 
| 89 90 | 
             
            files:
         | 
| 91 | 
            +
            - bin/lively
         | 
| 90 92 | 
             
            - lib/lively.rb
         | 
| 91 93 | 
             
            - lib/lively/application.rb
         | 
| 92 94 | 
             
            - lib/lively/assets.rb
         | 
| 93 95 | 
             
            - lib/lively/environment/application.rb
         | 
| 96 | 
            +
            - lib/lively/hello_world.rb
         | 
| 94 97 | 
             
            - lib/lively/pages/index.rb
         | 
| 95 98 | 
             
            - lib/lively/pages/index.xrb
         | 
| 96 99 | 
             
            - lib/lively/version.rb
         | 
| @@ -104,6 +107,7 @@ files: | |
| 104 107 | 
             
            - public/_components/morphdom/morphdom-umd.js
         | 
| 105 108 | 
             
            - public/_components/morphdom/morphdom-umd.min.js
         | 
| 106 109 | 
             
            - public/_components/morphdom/morphdom.js
         | 
| 110 | 
            +
            - public/_static/Falcon.png
         | 
| 107 111 | 
             
            - public/_static/icon.png
         | 
| 108 112 | 
             
            - public/_static/index.css
         | 
| 109 113 | 
             
            - public/_static/site.css
         | 
    
        metadata.gz.sig
    CHANGED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
             | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 1 | 
            +
            dW��8u�q�v_�CY:	����-��F�Ǒf�L���:Dž6�
         | 
| 2 | 
            +
            ��$�K~i)�:�/R/,�%�P�
         | 
| 3 | 
            +
            �0���A�W�f��Po�
         | 
| 4 | 
            +
            �;)j6k?r6=��`����Wi�h<�1T�9F�W�0���"�s��1c���z��b+��3���ͻ��%Ǚ��"'�v�&l��DނATV�l���L���Y������%����}�8�,��R�����p�:�y�7�+�v��r��za|�)�UrU۴/��D��"Z,��]���Ω�Z�|X�@��`�Eu�����R8����m^w�N�-��{�@L�
         |