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