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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2eb71ca34e43090f3b06b4577cc5c29ed82d3282df6fb26c69adb02cc5eff5fe
4
- data.tar.gz: 7df1288ae0defc42656da59f5a12e92f3619adc9993e54a4b5e55ac7d10cd795
3
+ metadata.gz: 5792cd3cb017762c79c0c8efcf99724db21dd444aa0e6e1f0c7f4f8e0e61774a
4
+ data.tar.gz: 0275c27630c5e0af1408771737e9fa87276ce3b6e2f06de788eaa9e45d60a6a5
5
5
  SHA512:
6
- metadata.gz: dafdbe14c26702f71de2bc519ac398d141ffc2d1b7f2d1b94f32c44602ea9efc6c083282268f48c7d08798269947322d13bc7005d664b02863fb7bf4f041f031
7
- data.tar.gz: d65fd9f4c15cd6992c67bf2ffb1ee21d4d86d15643b71e092f38f55386f438bea86e1aa1b0a535a1c0fbd48836e6ec99e77575e751eec513f7c74f77d89064f5
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)
@@ -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
- "Hello World"
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 index
32
- Pages::Index.new(title: self.title, body: self.body)
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 Protocol::HTTP::Response[200, [], [self.index.call]]
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, false)
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
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
6
  module Lively
7
- VERSION = "0.4.0"
7
+ VERSION = "0.6.0"
8
8
  end
@@ -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.window = window;
17
- this.document = window.document;
23
+ this.#window = window;
24
+ this.#document = window.document;
18
25
 
19
26
  this.url = url;
20
- this.events = [];
27
+ this.#server = null;
28
+ this.#events = [];
21
29
 
22
- this.failures = 0;
30
+ this.#failures = 0;
31
+ this.#reconnectTimer = null;
23
32
 
24
33
  // Track visibility state and connect if required:
25
- this.document.addEventListener("visibilitychange", () => this.handleVisibilityChange());
26
- this.handleVisibilityChange();
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.window.MutationObserver((mutationsList, observer) => {
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.unbind(node);
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.unbind(child);
53
+ this.#unbind(child);
40
54
  }
41
55
  }
42
56
 
43
57
  for (let node of mutation.addedNodes) {
44
- if (node.classList?.contains('live')) {
45
- this.bind(node);
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.bind(child);
66
+ this.#bind(child);
51
67
  }
52
68
  }
53
69
  }
54
70
  }
55
71
  });
56
72
 
57
- this.observer.observe(this.document.body, {childList: true, subtree: true});
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.server) return this.server;
79
+ if (this.#server) {
80
+ return this.#server;
81
+ }
66
82
 
67
- let server = this.server = new this.window.WebSocket(this.url);
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.failures = 0;
71
- this.flush();
91
+ this.#failures = 0;
92
+ this.#flush();
93
+ this.#attach();
72
94
  };
73
95
 
74
96
  server.onmessage = (message) => {
75
- const [name, ..._arguments] = JSON.parse(message.data);
97
+ const [name, ...args] = JSON.parse(message.data);
76
98
 
77
- this[name](..._arguments);
99
+ this[name](...args);
78
100
  };
79
101
 
80
102
  // The remote end has disconnected:
81
103
  server.addEventListener('error', () => {
82
- this.failures += 1;
104
+ this.#failures += 1;
83
105
  });
84
106
 
85
107
  server.addEventListener('close', () => {
86
- // Explicit disconnect will clear `this.server`:
87
- if (this.server) {
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.failures + 1) ** 2, 60000);
90
- setTimeout(() => this.connect(), delay);
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.server = null;
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.server) {
101
- const server = this.server;
102
- this.server = null;
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.server) {
139
+ #send(message) {
140
+ if (this.#server) {
109
141
  try {
110
- return this.server.send(message);
142
+ return this.#server.send(message);
111
143
  } catch (error) {
112
- // Ignore.
144
+ // console.log("Live.send", "failed to send message to server", error);
113
145
  }
114
146
  }
115
147
 
116
- this.events.push(message);
148
+ this.#events.push(message);
117
149
  }
118
150
 
119
- flush() {
120
- if (this.events.length === 0) return;
151
+ #flush() {
152
+ if (this.#events.length === 0) return;
121
153
 
122
- let events = this.events;
123
- this.events = [];
154
+ let events = this.#events;
155
+ this.#events = [];
124
156
 
125
157
  for (var event of events) {
126
- this.send(event);
158
+ this.#send(event);
127
159
  }
128
160
  }
129
161
 
130
- handleVisibilityChange() {
131
- if (this.document.hidden) {
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.send(JSON.stringify(['bind', element.id, element.dataset]));
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.send(JSON.stringify(['unbind', element.id]));
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.document.getElementsByClassName('live')) {
152
- this.bind(node);
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.document.createRange().createContextualFragment(html);
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.send(JSON.stringify(['reply', options.reply]));
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.document.getElementById(id);
170
- let fragment = this.createDocumentFragment(html);
215
+ let element = this.#document.getElementById(id);
216
+ let fragment = this.#createDocumentFragment(html);
171
217
 
172
218
  morphdom(element, fragment);
173
219
 
174
- this.reply(options);
220
+ this.#reply(options);
175
221
  }
176
222
 
177
223
  replace(selector, html, options) {
178
- let elements = this.document.querySelectorAll(selector);
179
- let fragment = this.createDocumentFragment(html);
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.reply(options);
229
+ this.#reply(options);
184
230
  }
185
231
 
186
232
  prepend(selector, html, options) {
187
- let elements = this.document.querySelectorAll(selector);
188
- let fragment = this.createDocumentFragment(html);
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.reply(options);
238
+ this.#reply(options);
193
239
  }
194
240
 
195
241
  append(selector, html, options) {
196
- let elements = this.document.querySelectorAll(selector);
197
- let fragment = this.createDocumentFragment(html);
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.reply(options);
247
+ this.#reply(options);
202
248
  }
203
249
 
204
250
  remove(selector, options) {
205
- let elements = this.document.querySelectorAll(selector);
251
+ let elements = this.#document.querySelectorAll(selector);
206
252
 
207
253
  elements.forEach(element => element.remove());
208
254
 
209
- this.reply(options);
255
+ this.#reply(options);
210
256
  }
211
257
 
212
258
  dispatchEvent(selector, type, options) {
213
- let elements = this.document.querySelectorAll(selector);
259
+ let elements = this.#document.querySelectorAll(selector);
214
260
 
215
261
  elements.forEach(element => element.dispatchEvent(
216
- new this.window.CustomEvent(type, options)
262
+ new this.#window.CustomEvent(type, options)
217
263
  ));
218
264
 
219
- this.reply(options);
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.send(
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@socketry/live",
3
3
  "type": "module",
4
- "version": "0.10.0",
4
+ "version": "0.13.0",
5
5
  "description": "Live HTML tags for Ruby.",
6
6
  "main": "Live.js",
7
7
  "repository": {
@@ -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
- var hidden = false;
56
- Object.defineProperty(dom.window.document, "hidden", {
57
- get() {return hidden},
123
+ live.connect();
124
+
125
+ const connected = new Promise(resolve => {
126
+ webSocketServer.on('connection', resolve);
58
127
  });
59
128
 
60
- live.handleVisibilityChange();
129
+ let socket = await connected;
61
130
 
62
- ok(live.server);
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
- hidden = true;
138
+ socket.send(
139
+ JSON.stringify(['script', 'my', 'throw new Error("Test Error")', {reply: true}])
140
+ );
65
141
 
66
- live.handleVisibilityChange();
142
+ let errorReply = await messages.popUntil(message => message[0] == 'reply');
143
+ strictEqual(errorReply[2], null);
144
+ console.log(errorReply);
67
145
 
68
- ok(!live.server);
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 reply;
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 reply;
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 reply;
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
@@ -1 +1,5 @@
1
- /* Default Stylesheet */
1
+ /*
2
+
3
+ Create your own `public/static/index.css` to override the default stylesheet.
4
+
5
+ */
@@ -1,6 +1,6 @@
1
1
 
2
2
  html {
3
- font-family: Monaco, monospace;
3
+ font-family: Arial, sans-serif;
4
4
  font-size: 16px;
5
5
 
6
6
  /* Fix odd text-size in `display: flex` elements on Safari iOS */
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.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-05 00:00:00.000000000 Z
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.8'
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.8'
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
- I���
2
- ��O�roT�J�nЉ�)X>_
3
- $,
4
- C*"\����4������84���_ o8z��������8LB 3+� ���.|��a��$�+�^�-�'rr��VuoX�%��3�@�F����B4�,��1����N���~t2H@�ӋEJ�.���ȗn'N7�����%�Fy����i��g�|�[a.PB�~v��]I���M���~�X>�V����`�6Xf�ݵ���; *ڕ��M0q�� �����y|p��h �P��wl���$gԴuyީ~ʚ{kf�Z ~#�5Pf#9=4_���3y_��x_��Zc�����%���o�;�<SdC!���/�������!�
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=��`����Wih<�1T9FW�0���"�s��1c���z��b+��3���ͻ��% Ǚ��"'�v�&l��DނATV�l���L���Y������%����}8�,�R�����p�:�y7�+�v��r��za|�)UrU۴/��D��"Z,��]���Ω�Z�|X�@��`�Eu�����R8����m^wN�-��{�@L�