talkdom 0.4.0 → 0.4.1
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.
- package/README.md +41 -1
- package/dist/talkdom-ws.esm.js +2 -0
- package/dist/talkdom-ws.esm.js.map +7 -0
- package/dist/talkdom-ws.min.js +2 -1
- package/dist/talkdom-ws.min.js.map +7 -1
- package/dist/talkdom.esm.js +2 -0
- package/dist/talkdom.esm.js.map +7 -0
- package/dist/talkdom.min.js +2 -1
- package/dist/talkdom.min.js.map +7 -1
- package/package.json +19 -8
- package/src/index.js +415 -0
- package/src/websocket.js +223 -0
- package/index.js +0 -366
- package/websocket.js +0 -234
package/websocket.js
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
(function () {
|
|
2
|
-
|
|
3
|
-
if (!window.talkDOM) {
|
|
4
|
-
console.error("talkdom-ws: load index.js before websocket.js");
|
|
5
|
-
return;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
var methods = talkDOM.methods;
|
|
9
|
-
var connections = Object.create(null);
|
|
10
|
-
var maxConnections = 16;
|
|
11
|
-
var BASE_DELAY = 1000;
|
|
12
|
-
var MAX_DELAY = 30000;
|
|
13
|
-
var CLEANUP_INTERVAL = 5000;
|
|
14
|
-
var WS = /\s+/;
|
|
15
|
-
|
|
16
|
-
// Parse receiver attribute to extract ws: URL.
|
|
17
|
-
// Returns the URL string or null if no ws: keyword found.
|
|
18
|
-
function parseWsUrl(attr) {
|
|
19
|
-
var tokens = attr.trim().split(WS);
|
|
20
|
-
for (var i = 1; i < tokens.length; i++) {
|
|
21
|
-
if (tokens[i] === "ws:" && tokens[i + 1]) return tokens[i + 1];
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Remove disconnected elements from a connection's receiver set.
|
|
27
|
-
// Returns true if any receivers remain.
|
|
28
|
-
function pruneReceivers(conn) {
|
|
29
|
-
conn.receivers.forEach(function (el) {
|
|
30
|
-
if (!el.isConnected) conn.receivers.delete(el);
|
|
31
|
-
});
|
|
32
|
-
return conn.receivers.size > 0;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Fire a custom event on all connected receivers for a URL.
|
|
36
|
-
function fireEvent(conn, name, detail) {
|
|
37
|
-
conn.receivers.forEach(function (el) {
|
|
38
|
-
if (el.isConnected) {
|
|
39
|
-
el.dispatchEvent(new CustomEvent(name, { bubbles: true, detail: detail }));
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Route a parsed JSON message to matching receiver elements.
|
|
45
|
-
function routeJson(conn, msg) {
|
|
46
|
-
var name = msg.receiver;
|
|
47
|
-
var op = msg.op || "inner";
|
|
48
|
-
var content = msg.content || "";
|
|
49
|
-
var targets;
|
|
50
|
-
if (name) {
|
|
51
|
-
targets = document.querySelectorAll('[receiver~="' + name + '"]');
|
|
52
|
-
} else {
|
|
53
|
-
// Broadcast to all receivers on this connection.
|
|
54
|
-
targets = Array.from(conn.receivers);
|
|
55
|
-
}
|
|
56
|
-
var detail = { receiver: name || "", selector: "apply:", args: [content, op] };
|
|
57
|
-
for (var i = 0; i < targets.length; i++) {
|
|
58
|
-
var el = targets[i];
|
|
59
|
-
methods["apply:"](el, content, op);
|
|
60
|
-
el.dispatchEvent(new CustomEvent("talkdom:done", { bubbles: true, detail: detail }));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Handle an incoming WebSocket message.
|
|
65
|
-
function onMessage(url, event) {
|
|
66
|
-
var conn = connections[url];
|
|
67
|
-
if (!conn) return;
|
|
68
|
-
pruneReceivers(conn);
|
|
69
|
-
var data = event.data;
|
|
70
|
-
if (typeof data !== "string") return; // ignore binary
|
|
71
|
-
if (data.charAt(0) === "{") {
|
|
72
|
-
try {
|
|
73
|
-
var msg = JSON.parse(data);
|
|
74
|
-
routeJson(conn, msg);
|
|
75
|
-
} catch (e) {
|
|
76
|
-
console.error("talkdom-ws: invalid JSON from " + url, e);
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
// Raw talkDOM message syntax, dispatch through core.
|
|
80
|
-
talkDOM.send(data).catch(function (err) {
|
|
81
|
-
console.warn("talkdom-ws:", err);
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function scheduleReconnect(url) {
|
|
87
|
-
var conn = connections[url];
|
|
88
|
-
if (!conn) return;
|
|
89
|
-
if (!pruneReceivers(conn)) { cleanup(url); return; }
|
|
90
|
-
var delay = Math.min(conn.backoff, MAX_DELAY);
|
|
91
|
-
delay = delay * (0.75 + Math.random() * 0.5);
|
|
92
|
-
conn.timer = setTimeout(function () {
|
|
93
|
-
conn.backoff = Math.min(conn.backoff * 2, MAX_DELAY);
|
|
94
|
-
connectWs(url);
|
|
95
|
-
}, delay);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function cleanup(url) {
|
|
99
|
-
var conn = connections[url];
|
|
100
|
-
if (!conn) return;
|
|
101
|
-
if (conn.timer) clearTimeout(conn.timer);
|
|
102
|
-
if (conn.checkTimer) clearInterval(conn.checkTimer);
|
|
103
|
-
if (conn.ws) {
|
|
104
|
-
conn.ws.onclose = null; // prevent reconnect on intentional close
|
|
105
|
-
conn.ws.close();
|
|
106
|
-
}
|
|
107
|
-
delete connections[url];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function connectWs(url) {
|
|
111
|
-
var conn = connections[url];
|
|
112
|
-
if (!conn) return;
|
|
113
|
-
// Already open or connecting — skip.
|
|
114
|
-
if (conn.ws && (conn.ws.readyState === WebSocket.OPEN || conn.ws.readyState === WebSocket.CONNECTING)) return;
|
|
115
|
-
|
|
116
|
-
var ws = new WebSocket(url);
|
|
117
|
-
|
|
118
|
-
ws.onopen = function () {
|
|
119
|
-
conn.backoff = BASE_DELAY;
|
|
120
|
-
fireEvent(conn, "talkdom:ws:open", { url: url });
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
ws.onmessage = function (e) {
|
|
124
|
-
onMessage(url, e);
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
ws.onclose = function (e) {
|
|
128
|
-
fireEvent(conn, "talkdom:ws:close", { url: url, code: e.code, reason: e.reason });
|
|
129
|
-
scheduleReconnect(url);
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
ws.onerror = function () {
|
|
133
|
-
fireEvent(conn, "talkdom:ws:error", { url: url });
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
conn.ws = ws;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Subscribe an element to a WebSocket URL.
|
|
140
|
-
function subscribe(el, url) {
|
|
141
|
-
var conn = connections[url];
|
|
142
|
-
if (!conn) {
|
|
143
|
-
var count = Object.keys(connections).length;
|
|
144
|
-
if (count >= maxConnections) {
|
|
145
|
-
console.warn("talkdom-ws: max connections (" + maxConnections + ") reached, ignoring " + url);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
conn = { ws: null, receivers: new Set(), backoff: BASE_DELAY, timer: null, checkTimer: null };
|
|
149
|
-
connections[url] = conn;
|
|
150
|
-
// Periodic cleanup check for this connection.
|
|
151
|
-
conn.checkTimer = setInterval(function () {
|
|
152
|
-
if (!pruneReceivers(conn)) cleanup(url);
|
|
153
|
-
}, CLEANUP_INTERVAL);
|
|
154
|
-
}
|
|
155
|
-
conn.receivers.add(el);
|
|
156
|
-
connectWs(url);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Scan a single element for ws: keyword and subscribe.
|
|
160
|
-
function initElement(el) {
|
|
161
|
-
var attr = el.getAttribute("receiver");
|
|
162
|
-
if (!attr) return;
|
|
163
|
-
var url = parseWsUrl(attr);
|
|
164
|
-
if (!url) return;
|
|
165
|
-
subscribe(el, url);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Scan all existing receiver elements.
|
|
169
|
-
function initWsReceivers() {
|
|
170
|
-
document.querySelectorAll("[receiver]").forEach(initElement);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Watch for dynamically added ws: receivers.
|
|
174
|
-
new MutationObserver(function (mutations) {
|
|
175
|
-
for (var i = 0; i < mutations.length; i++) {
|
|
176
|
-
var added = mutations[i].addedNodes;
|
|
177
|
-
for (var j = 0; j < added.length; j++) {
|
|
178
|
-
var node = added[j];
|
|
179
|
-
if (node.nodeType !== 1) continue;
|
|
180
|
-
if (node.hasAttribute && node.hasAttribute("receiver")) initElement(node);
|
|
181
|
-
if (node.querySelectorAll) {
|
|
182
|
-
node.querySelectorAll("[receiver]").forEach(initElement);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}).observe(document, { childList: true, subtree: true });
|
|
187
|
-
|
|
188
|
-
// ws:send: method — send element value over an existing WebSocket connection.
|
|
189
|
-
methods["ws:send:"] = function (el, url) {
|
|
190
|
-
var conn = connections[url];
|
|
191
|
-
if (!conn || !conn.ws || conn.ws.readyState !== WebSocket.OPEN) {
|
|
192
|
-
console.error("talkdom-ws: no open connection to " + url);
|
|
193
|
-
return Promise.reject("not connected");
|
|
194
|
-
}
|
|
195
|
-
var payload = ("value" in el) ? el.value : el.textContent;
|
|
196
|
-
conn.ws.send(payload);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
initWsReceivers();
|
|
200
|
-
|
|
201
|
-
talkDOM.ws = {
|
|
202
|
-
connect: function (url) {
|
|
203
|
-
if (!connections[url]) {
|
|
204
|
-
var count = Object.keys(connections).length;
|
|
205
|
-
if (count >= maxConnections) {
|
|
206
|
-
console.warn("talkdom-ws: max connections (" + maxConnections + ") reached");
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
connections[url] = { ws: null, receivers: new Set(), backoff: BASE_DELAY, timer: null, checkTimer: null };
|
|
210
|
-
connections[url].checkTimer = setInterval(function () {
|
|
211
|
-
if (!pruneReceivers(connections[url])) cleanup(url);
|
|
212
|
-
}, CLEANUP_INTERVAL);
|
|
213
|
-
}
|
|
214
|
-
connectWs(url);
|
|
215
|
-
},
|
|
216
|
-
disconnect: function (url) { cleanup(url); },
|
|
217
|
-
send: function (url, data) {
|
|
218
|
-
var conn = connections[url];
|
|
219
|
-
if (!conn || !conn.ws || conn.ws.readyState !== WebSocket.OPEN) return false;
|
|
220
|
-
conn.ws.send(typeof data === "string" ? data : JSON.stringify(data));
|
|
221
|
-
return true;
|
|
222
|
-
},
|
|
223
|
-
get connections() {
|
|
224
|
-
var out = Object.create(null);
|
|
225
|
-
for (var url in connections) {
|
|
226
|
-
out[url] = { state: connections[url].ws ? connections[url].ws.readyState : -1, receivers: connections[url].receivers.size };
|
|
227
|
-
}
|
|
228
|
-
return out;
|
|
229
|
-
},
|
|
230
|
-
get maxConnections() { return maxConnections; },
|
|
231
|
-
set maxConnections(n) { maxConnections = n; },
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
}());
|