lively 0.3.0 → 0.5.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: 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