lively 0.3.0 → 0.5.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: 193f180461ac23de0fd1401f41e1c3b5c09b5d770326b7c9bab0958e7a3ad1f8
4
- data.tar.gz: 716af747f77e13785c2753a0710282828dbfc4d436583fe9b4d53747eb5f2f19
3
+ metadata.gz: 1fe993ef2758a622d5da68c03094ea040678f62618b26bd8f888ab6127366300
4
+ data.tar.gz: 4919d6e2ec9fd70f2388d838a98740a6f911875ee233e3d351e55e59f426fa46
5
5
  SHA512:
6
- metadata.gz: 38e46574582d3439c82f7d6241103ebaea644f8fd2df4e97e63b970952a8a1d7b5aea49556b9635d6dbb17890bf971ab869cabec023671d083c94f28a6cd0b62
7
- data.tar.gz: 9178f2ceeae957d72d85d73590ed2a40edec2de72ba5545048a460bb37596adada9c81ef6844f6e443222e67d2ddc992abfda4fcbdfebfdae36e7fdd5d1535fb
6
+ metadata.gz: 1b8e0c1657776e309f5e164f79ad66e345aac9a455ef604bf2c326dfef9ce06e2a8f8370cd6052706a825bbf159ce5e5f60ade6b3cfd5f651c38aa2ee3c15bc6
7
+ data.tar.gz: 2f2e3e3043956ae4ce65c5a82e82c62af462d1b750ef51d0aa4dc71c9ffc0f731ad80779517fe44263b5c9f57a1d187f84e2cb9d0a325c46855c799c9d404c3d
checksums.yaml.gz.sig CHANGED
Binary file
@@ -4,5 +4,5 @@
4
4
  # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
6
  module Lively
7
- VERSION = "0.3.0"
7
+ VERSION = "0.5.0"
8
8
  end
@@ -1,4 +1,3 @@
1
-
2
1
  import morphdom from 'morphdom';
3
2
 
4
3
  export class Live {
@@ -21,21 +20,67 @@ export class Live {
21
20
  this.events = [];
22
21
 
23
22
  this.failures = 0;
23
+ this.reconnectTimer = null;
24
24
 
25
25
  // Track visibility state and connect if required:
26
26
  this.document.addEventListener("visibilitychange", () => this.handleVisibilityChange());
27
27
  this.handleVisibilityChange();
28
+
29
+ const elementNodeType = this.window.Node.ELEMENT_NODE;
30
+
31
+ // Create a MutationObserver to watch for removed nodes
32
+ this.observer = new this.window.MutationObserver((mutationsList, observer) => {
33
+ for (let mutation of mutationsList) {
34
+ if (mutation.type === 'childList') {
35
+ for (let node of mutation.removedNodes) {
36
+ if (node.nodeType !== elementNodeType) continue;
37
+
38
+ if (node.classList?.contains('live')) {
39
+ this.unbind(node);
40
+ }
41
+
42
+ // Unbind any child nodes:
43
+ for (let child of node.getElementsByClassName('live')) {
44
+ this.unbind(child);
45
+ }
46
+ }
47
+
48
+ for (let node of mutation.addedNodes) {
49
+ if (node.nodeType !== elementNodeType) continue;
50
+
51
+ if (node.classList.contains('live')) {
52
+ this.bind(node);
53
+ }
54
+
55
+ // Bind any child nodes:
56
+ for (let child of node.getElementsByClassName('live')) {
57
+ this.bind(child);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ });
63
+
64
+ this.observer.observe(this.document.body, {childList: true, subtree: true});
28
65
  }
29
66
 
30
67
  // -- Connection Handling --
31
68
 
32
69
  connect() {
33
- if (this.server) return this.server;
70
+ if (this.server) {
71
+ return this.server;
72
+ }
34
73
 
35
74
  let server = this.server = new this.window.WebSocket(this.url);
36
75
 
76
+ if (this.reconnectTimer) {
77
+ clearTimeout(this.reconnectTimer);
78
+ this.reconnectTimer = null;
79
+ }
80
+
37
81
  server.onopen = () => {
38
82
  this.failures = 0;
83
+ this.flush();
39
84
  this.attach();
40
85
  };
41
86
 
@@ -52,13 +97,18 @@ export class Live {
52
97
 
53
98
  server.addEventListener('close', () => {
54
99
  // Explicit disconnect will clear `this.server`:
55
- if (this.server) {
100
+ if (this.server && !this.reconnectTimer) {
56
101
  // We need a minimum delay otherwise this can end up immediately invoking the callback:
57
102
  const delay = Math.max(100 * (this.failures + 1) ** 2, 60000);
58
- setTimeout(() => this.connect(), delay);
103
+ this.reconnectTimer = setTimeout(() => {
104
+ this.reconnectTimer = null;
105
+ this.connect();
106
+ }, delay);
59
107
  }
60
108
 
61
- this.server = null;
109
+ if (this.server === server) {
110
+ this.server = null;
111
+ }
62
112
  });
63
113
 
64
114
  return server;
@@ -70,14 +120,23 @@ export class Live {
70
120
  this.server = null;
71
121
  server.close();
72
122
  }
123
+
124
+ if (this.reconnectTimer) {
125
+ clearTimeout(this.reconnectTimer);
126
+ this.reconnectTimer = null;
127
+ }
73
128
  }
74
129
 
75
130
  send(message) {
76
- try {
77
- this.server.send(message);
78
- } catch (error) {
79
- this.events.push(message);
131
+ if (this.server) {
132
+ try {
133
+ return this.server.send(message);
134
+ } catch (error) {
135
+ // console.log("Live.send", "failed to send message to server", error);
136
+ }
80
137
  }
138
+
139
+ this.events.push(message);
81
140
  }
82
141
 
83
142
  flush() {
@@ -91,33 +150,31 @@ export class Live {
91
150
  }
92
151
  }
93
152
 
94
- bind(elements) {
95
- for (var element of elements) {
96
- this.send(JSON.stringify({bind: element.id, data: element.dataset}));
153
+ handleVisibilityChange() {
154
+ if (this.document.hidden) {
155
+ this.disconnect();
156
+ } else {
157
+ this.connect();
97
158
  }
98
159
  }
99
160
 
100
- bindElementsByClassName(selector = 'live') {
101
- this.bind(
102
- this.document.getElementsByClassName(selector)
103
- );
161
+ bind(element) {
162
+ console.log("bind", element.id, element.dataset);
104
163
 
105
- this.flush();
164
+ this.send(JSON.stringify(['bind', element.id, element.dataset]));
106
165
  }
107
166
 
108
- handleVisibilityChange() {
109
- if (this.document.hidden) {
110
- this.disconnect();
111
- } else {
112
- this.connect();
167
+ unbind(element) {
168
+ console.log("unbind", element.id, element.dataset);
169
+
170
+ if (this.server) {
171
+ this.send(JSON.stringify(['unbind', element.id]));
113
172
  }
114
173
  }
115
174
 
116
175
  attach() {
117
- if (this.document.readyState === 'loading') {
118
- this.document.addEventListener('DOMContentLoaded', () => this.bindElementsByClassName());
119
- } else {
120
- this.bindElementsByClassName();
176
+ for (let node of this.document.getElementsByClassName('live')) {
177
+ this.bind(node);
121
178
  }
122
179
  }
123
180
 
@@ -126,8 +183,8 @@ export class Live {
126
183
  }
127
184
 
128
185
  reply(options) {
129
- if (options && options.reply) {
130
- this.send(JSON.stringify({reply: options.reply}));
186
+ if (options?.reply) {
187
+ this.send(JSON.stringify(['reply', options.reply]));
131
188
  }
132
189
  }
133
190
 
@@ -193,7 +250,7 @@ export class Live {
193
250
  this.connect();
194
251
 
195
252
  this.send(
196
- JSON.stringify({id: id, event: event})
253
+ JSON.stringify(['event', id, event])
197
254
  );
198
255
  }
199
256
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@socketry/live",
3
3
  "type": "module",
4
- "version": "0.7.0",
4
+ "version": "0.11.0",
5
5
  "description": "Live HTML tags for Ruby.",
6
6
  "main": "Live.js",
7
7
  "repository": {
@@ -1,29 +1,80 @@
1
- import {describe, before, after, it} from 'node:test';
2
- import {ok, strict, strictEqual} from 'node:assert';
1
+ import {describe, before, beforeEach, after, it} from 'node:test';
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`;
14
53
 
15
54
  before(async function () {
16
- const listening = new Promise(resolve => {
55
+ const listening = new Promise((resolve, reject) => {
17
56
  webSocketServer = new WebSocket.Server(webSocketServerConfig, resolve);
57
+ webSocketServer.on('error', reject);
18
58
  });
19
59
 
20
- 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>');
21
61
  // Ensure the WebSocket class is available:
22
62
  dom.window.WebSocket = WebSocket;
23
63
 
24
64
  await new Promise(resolve => dom.window.addEventListener('load', resolve));
25
65
 
26
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();
27
78
  });
28
79
 
29
80
  after(function () {
@@ -48,23 +99,31 @@ describe('Live', function () {
48
99
  live.disconnect();
49
100
  });
50
101
 
51
- it('should handle visibility changes', function () {
102
+ it('should handle visibility changes', async function () {
52
103
  const live = new Live(dom.window, webSocketServerURL);
53
104
 
54
- var hidden = false;
105
+ let hidden = false;
55
106
  Object.defineProperty(dom.window.document, "hidden", {
56
107
  get() {return hidden},
57
108
  });
58
109
 
110
+ // The document starts out hidden... we have defined a property to make it not hidden, let's propagate that change:
59
111
  live.handleVisibilityChange();
60
112
 
61
- ok(live.server);
113
+ // We should receive a bind message for the live element:
114
+ deepStrictEqual(await messages.pop(), ['bind', 'my', {}]);
62
115
 
63
116
  hidden = true;
117
+ live.handleVisibilityChange();
118
+ ok(!live.server);
64
119
 
120
+ hidden = false;
65
121
  live.handleVisibilityChange();
122
+ ok(live.server);
66
123
 
67
- ok(!live.server);
124
+ deepStrictEqual(await messages.pop(), ['bind', 'my', {}]);
125
+
126
+ live.disconnect();
68
127
  });
69
128
 
70
129
  it('should handle updates', async function () {
@@ -78,25 +137,18 @@ describe('Live', function () {
78
137
 
79
138
  let socket = await connected;
80
139
 
81
- const reply = new Promise((resolve, reject) => {
82
- socket.on('message', message => {
83
- let payload = JSON.parse(message);
84
- if (payload.reply) resolve(payload);
85
- });
86
- });
87
-
88
140
  socket.send(
89
141
  JSON.stringify(['update', 'my', '<div id="my"><p>Goodbye World!</p></div>', {reply: true}])
90
142
  );
91
143
 
92
- await reply;
144
+ await messages.popUntil(message => message[0] == 'reply');
93
145
 
94
146
  strictEqual(dom.window.document.getElementById('my').innerHTML, '<p>Goodbye World!</p>');
95
147
 
96
148
  live.disconnect();
97
149
  });
98
150
 
99
- it('should handle replacements', async function () {
151
+ it('should handle updates with child live elements', async function () {
100
152
  const live = new Live(dom.window, webSocketServerURL);
101
153
 
102
154
  live.connect();
@@ -107,19 +159,49 @@ describe('Live', function () {
107
159
 
108
160
  let socket = await connected;
109
161
 
110
- const reply = new Promise((resolve, reject) => {
111
- socket.on('message', message => {
112
- let payload = JSON.parse(message);
113
- if (payload.reply) resolve(payload);
114
- });
162
+ socket.send(
163
+ JSON.stringify(['update', 'my', '<div id="my"><div id="mychild" class="live"></div></div>'])
164
+ );
165
+
166
+ let payload = await messages.popUntil(message => message[0] == 'bind');
167
+ deepStrictEqual(payload, ['bind', 'mychild', {}]);
168
+
169
+ live.disconnect();
170
+ });
171
+
172
+ it('can unbind removed elements', async function () {
173
+ dom.window.document.body.innerHTML = '<div id="my" class="live"><p>Hello World</p></div>';
174
+
175
+ const live = new Live(dom.window, webSocketServerURL);
176
+
177
+ live.connect();
178
+
179
+ dom.window.document.getElementById('my').remove();
180
+
181
+ let payload = await messages.popUntil(message => message[0] == 'unbind');
182
+ deepStrictEqual(payload, ['unbind', 'my']);
183
+
184
+ live.disconnect();
185
+ });
186
+
187
+ it('should handle replacements', async function () {
188
+ dom.window.document.body.innerHTML = '<div id="my"><p>Hello World</p></div>';
189
+
190
+ const live = new Live(dom.window, webSocketServerURL);
191
+
192
+ live.connect();
193
+
194
+ const connected = new Promise(resolve => {
195
+ webSocketServer.on('connection', resolve);
115
196
  });
116
197
 
198
+ let socket = await connected;
199
+
117
200
  socket.send(
118
201
  JSON.stringify(['replace', '#my p', '<p>Replaced!</p>', {reply: true}])
119
202
  );
120
203
 
121
- await reply;
122
-
204
+ await messages.popUntil(message => message[0] == 'reply');
123
205
  strictEqual(dom.window.document.getElementById('my').innerHTML, '<p>Replaced!</p>');
124
206
 
125
207
  live.disconnect();
@@ -140,19 +222,11 @@ describe('Live', function () {
140
222
  JSON.stringify(['update', 'my', '<div id="my"><p>Middle</p></div>'])
141
223
  );
142
224
 
143
- const reply = new Promise((resolve, reject) => {
144
- socket.on('message', message => {
145
- let payload = JSON.parse(message);
146
- if (payload.reply) resolve(payload);
147
- });
148
- });
149
-
150
225
  socket.send(
151
226
  JSON.stringify(['prepend', '#my', '<p>Prepended!</p>', {reply: true}])
152
227
  );
153
228
 
154
- await reply;
155
-
229
+ await messages.popUntil(message => message[0] == 'reply');
156
230
  strictEqual(dom.window.document.getElementById('my').innerHTML, '<p>Prepended!</p><p>Middle</p>');
157
231
 
158
232
  live.disconnect();
@@ -173,19 +247,11 @@ describe('Live', function () {
173
247
  JSON.stringify(['update', 'my', '<div id="my"><p>Middle</p></div>'])
174
248
  );
175
249
 
176
- const reply = new Promise((resolve, reject) => {
177
- socket.on('message', message => {
178
- let payload = JSON.parse(message);
179
- if (payload.reply) resolve(payload);
180
- });
181
- });
182
-
183
250
  socket.send(
184
251
  JSON.stringify(['append', '#my', '<p>Appended!</p>', {reply: true}])
185
252
  );
186
253
 
187
- await reply;
188
-
254
+ await messages.popUntil(message => message[0] == 'reply');
189
255
  strictEqual(dom.window.document.getElementById('my').innerHTML, '<p>Middle</p><p>Appended!</p>');
190
256
 
191
257
  live.disconnect();
@@ -206,19 +272,11 @@ describe('Live', function () {
206
272
  JSON.stringify(['update', 'my', '<div id="my"><p>Middle</p></div>'])
207
273
  );
208
274
 
209
- const reply = new Promise((resolve, reject) => {
210
- socket.on('message', message => {
211
- let payload = JSON.parse(message);
212
- if (payload.reply) resolve(payload);
213
- });
214
- });
215
-
216
275
  socket.send(
217
276
  JSON.stringify(['remove', '#my p', {reply: true}])
218
277
  );
219
278
 
220
- await reply;
221
-
279
+ await messages.popUntil(message => message[0] == 'reply');
222
280
  strictEqual(dom.window.document.getElementById('my').innerHTML, '');
223
281
 
224
282
  live.disconnect();
@@ -235,18 +293,11 @@ describe('Live', function () {
235
293
 
236
294
  let socket = await connected;
237
295
 
238
- const reply = new Promise((resolve, reject) => {
239
- socket.on('message', message => {
240
- let payload = JSON.parse(message);
241
- if (payload.reply) resolve(payload);
242
- });
243
- });
244
-
245
296
  socket.send(
246
297
  JSON.stringify(['dispatchEvent', '#my', 'click', {reply: true}])
247
298
  );
248
299
 
249
- await reply;
300
+ await messages.popUntil(message => message[0] == 'reply');
250
301
 
251
302
  live.disconnect();
252
303
  });
@@ -262,23 +313,15 @@ describe('Live', function () {
262
313
 
263
314
  let socket = await connected;
264
315
 
265
- const reply = new Promise((resolve, reject) => {
266
- socket.on('message', message => {
267
- let payload = JSON.parse(message);
268
- if (payload.event) resolve(payload);
269
- });
270
- });
271
-
272
316
  dom.window.document.getElementById('my').addEventListener('click', event => {
273
317
  live.forwardEvent('my', event);
274
318
  });
275
319
 
276
320
  dom.window.document.getElementById('my').click();
277
321
 
278
- let payload = await reply;
279
-
280
- strictEqual(payload.id, 'my');
281
- strictEqual(payload.event.type, 'click');
322
+ let payload = await messages.popUntil(message => message[0] == 'event');
323
+ strictEqual(payload[1], 'my');
324
+ strictEqual(payload[2].type, 'click');
282
325
 
283
326
  live.disconnect();
284
327
  });
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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -37,36 +37,36 @@ 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
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - ">="
46
+ - - "~>"
47
47
  - !ruby/object:Gem::Version
48
- version: '0'
48
+ version: '0.47'
49
49
  type: :runtime
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - ">="
53
+ - - "~>"
54
54
  - !ruby/object:Gem::Version
55
- version: '0'
55
+ version: '0.47'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: live
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
60
  - - "~>"
61
61
  - !ruby/object:Gem::Version
62
- version: 0.7.0
62
+ version: '0.8'
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.7.0
69
+ version: '0.8'
70
70
  - !ruby/object:Gem::Dependency
71
71
  name: xrb
72
72
  requirement: !ruby/object:Gem::Requirement
metadata.gz.sig CHANGED
Binary file