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�
|