lively 0.4.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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�
|