@cocreate/elements 1.46.0 → 1.48.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.
- package/CONTRIBUTING.md +1 -1
- package/README.md +467 -44
- package/package.json +9 -8
- package/src/crudHandler.js +150 -46
- package/src/dndHandler.js +11 -8
- package/src/formHandler.js +225 -170
- package/src/getDataHandler.js +111 -53
- package/src/index.js +4 -3
- package/src/initHandler.js +96 -46
- package/src/mapRegistry.js +10 -1
- package/src/setDataHandler.js +163 -84
- package/src/socketHandler.js +328 -0
- package/src/srcHandler.js +68 -32
- package/src/subscribeHandler.js +64 -64
- package/test/deferred-src.test.cjs +11 -0
- package/test/form-enter.test.cjs +48 -0
- package/test/get-data.test.cjs +100 -0
- package/test/init-read.test.cjs +145 -0
- package/test/load-more.test.cjs +19 -0
- package/test/save-array.test.cjs +31 -0
- package/test/save-completion.test.cjs +36 -0
- package/test/set-data-type.test.cjs +119 -0
- package/test/ssr-response.test.cjs +108 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import Actions from "@cocreate/actions";
|
|
2
|
+
import Observer from "@cocreate/observer";
|
|
3
|
+
import Socket from "@cocreate/socket-client";
|
|
4
|
+
import { dotNotationToObject, getValueFromObject, queryData, uuid } from "@cocreate/utils";
|
|
5
|
+
|
|
6
|
+
const socketOnSelector = "[socket-on]";
|
|
7
|
+
const socketOnElements = new WeakMap();
|
|
8
|
+
const socketOnListeners = new Map();
|
|
9
|
+
const socketStatusSelector = "[socket-status]";
|
|
10
|
+
const socketStatusElements = new Set();
|
|
11
|
+
let socketStatusListenersInitialized = false;
|
|
12
|
+
|
|
13
|
+
function getSocketScope(element, form) {
|
|
14
|
+
const scopedElement = element?.closest("[socket]");
|
|
15
|
+
if (scopedElement && scopedElement !== element) return scopedElement;
|
|
16
|
+
return form || scopedElement || element;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getSocketFields(scope, element) {
|
|
20
|
+
if (!scope) return element ? [element] : [];
|
|
21
|
+
|
|
22
|
+
const fields = [];
|
|
23
|
+
if (scope.matches?.("[socket-key], [send]")) fields.push(scope);
|
|
24
|
+
fields.push(...scope.querySelectorAll?.("[socket-key], [send]") || []);
|
|
25
|
+
|
|
26
|
+
return fields.filter((field) => {
|
|
27
|
+
if (field.closest("[template]")) return false;
|
|
28
|
+
const key = field.getAttribute("socket-key") || field.getAttribute("send");
|
|
29
|
+
if (!key) return false;
|
|
30
|
+
|
|
31
|
+
// Keep nested socket scopes independent, just like API endpoint groups.
|
|
32
|
+
const fieldScope = field.closest("[socket]");
|
|
33
|
+
return !fieldScope || fieldScope === scope;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function getData({ element, form, scope }) {
|
|
38
|
+
const fields = getSocketFields(scope, element);
|
|
39
|
+
const flatData = {};
|
|
40
|
+
|
|
41
|
+
for (const field of fields) {
|
|
42
|
+
const key = field.getAttribute("socket-key") || field.getAttribute("send");
|
|
43
|
+
if (!key || typeof field.getValue !== "function") continue;
|
|
44
|
+
flatData[key] = await field.getValue();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return dotNotationToObject(flatData);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function dispatchSocketError(element, form, error) {
|
|
51
|
+
element?.dispatchEvent?.(
|
|
52
|
+
new CustomEvent("socketerror", {
|
|
53
|
+
bubbles: true,
|
|
54
|
+
detail: { error, element, form }
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getResponseError(response) {
|
|
60
|
+
if (!response?.error && response?.success !== false && response?.ai?.status !== "error") return null;
|
|
61
|
+
return new Error(
|
|
62
|
+
response.ai?.message || response.error || "Socket request failed."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function send({ element, form, continue: next, abort }) {
|
|
67
|
+
const scope = getSocketScope(element, form);
|
|
68
|
+
const method = scope?.getAttribute?.("socket") || element?.getAttribute?.("socket");
|
|
69
|
+
const checkbox = element?.matches?.('input[type="checkbox"]') ? element : null;
|
|
70
|
+
const previousChecked = checkbox ? !checkbox.checked : null;
|
|
71
|
+
const wasDisabled = checkbox?.disabled;
|
|
72
|
+
if (checkbox) checkbox.disabled = true;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const data = await getData({ element, form, scope });
|
|
76
|
+
const request = { ...data };
|
|
77
|
+
if (method) request.method = method;
|
|
78
|
+
if (!request.uuid) request.uuid = uuid();
|
|
79
|
+
const isAIChat = request.method === "ai.chat";
|
|
80
|
+
if (isAIChat) {
|
|
81
|
+
console.info("CoCreate AI socket request", {
|
|
82
|
+
uuid: request.uuid,
|
|
83
|
+
conversation_id: request.ai?.conversation_id,
|
|
84
|
+
user_message_id: request.ai?.user_message_id
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const response = await Socket.send(request);
|
|
89
|
+
if (response?.uuid !== request.uuid) {
|
|
90
|
+
throw new Error("Socket response UUID did not match the request.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (isAIChat) {
|
|
94
|
+
console.info("CoCreate AI socket response", {
|
|
95
|
+
uuid: request.uuid,
|
|
96
|
+
response_uuid: response.uuid,
|
|
97
|
+
status: response.ai?.status || response.status,
|
|
98
|
+
user_message_id: response.ai?.user_message_id
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const responseError = getResponseError(response);
|
|
102
|
+
if (responseError) {
|
|
103
|
+
if (checkbox) checkbox.checked = previousChecked;
|
|
104
|
+
dispatchSocketError(element, form, responseError);
|
|
105
|
+
abort?.();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
next?.();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (checkbox) checkbox.checked = previousChecked;
|
|
111
|
+
dispatchSocketError(element, form, error);
|
|
112
|
+
abort?.();
|
|
113
|
+
} finally {
|
|
114
|
+
if (checkbox) checkbox.disabled = wasDisabled;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function parseSocketMatches(element) {
|
|
119
|
+
const value = element.getAttribute("socket-on-matches");
|
|
120
|
+
if (!value) return {};
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const matches = JSON.parse(value);
|
|
124
|
+
if (!matches || typeof matches !== "object" || Array.isArray(matches)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return matches;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.warn("CoCreate: socket-on-matches must be a JSON object.", error);
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeSocketMatches(query, prefix = "") {
|
|
135
|
+
const normalized = {};
|
|
136
|
+
const logicalOperators = new Set(["$and", "$or", "$nor"]);
|
|
137
|
+
|
|
138
|
+
for (const [key, value] of Object.entries(query)) {
|
|
139
|
+
if (!prefix && logicalOperators.has(key)) {
|
|
140
|
+
normalized[key] = Array.isArray(value)
|
|
141
|
+
? value.map((entry) => normalizeSocketMatches(entry))
|
|
142
|
+
: value;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
147
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
148
|
+
const properties = Object.keys(value);
|
|
149
|
+
if (properties.some((property) => property.startsWith("$"))) {
|
|
150
|
+
normalized[path] = value;
|
|
151
|
+
} else {
|
|
152
|
+
Object.assign(normalized, normalizeSocketMatches(value, path));
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
normalized[path] = value;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return normalized;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function matchesSocketMessage(element, data) {
|
|
163
|
+
const matches = parseSocketMatches(element);
|
|
164
|
+
if (!matches) return false;
|
|
165
|
+
try {
|
|
166
|
+
return queryData(data, normalizeSocketMatches(matches));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.warn("CoCreate: unable to evaluate socket-on-matches query.", error);
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function setSocketOnValue(element, data) {
|
|
174
|
+
if (!element.isConnected || !matchesSocketMessage(element, data)) return;
|
|
175
|
+
const key = element.getAttribute("socket-on-key");
|
|
176
|
+
if (!key || typeof element.setValue !== "function") return;
|
|
177
|
+
|
|
178
|
+
const value = getValueFromObject(data, key);
|
|
179
|
+
if (value === undefined) return;
|
|
180
|
+
if (typeof element.renderValue === "function") {
|
|
181
|
+
await element.renderValue({ object: value });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
await element.setValue(value);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function setSocketStatus(element, status) {
|
|
188
|
+
if (!element.isConnected) return;
|
|
189
|
+
const label = element.getAttribute(`socket-status-${status}`);
|
|
190
|
+
if (label) element.textContent = label;
|
|
191
|
+
element.classList.toggle("connected", status === "connected");
|
|
192
|
+
element.classList.toggle("disconnected", status === "disconnected");
|
|
193
|
+
element.classList.remove("error");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function registerSocketStatus(element) {
|
|
197
|
+
if (!element?.matches?.(socketStatusSelector)) return;
|
|
198
|
+
socketStatusElements.add(element);
|
|
199
|
+
if (!socketStatusListenersInitialized) {
|
|
200
|
+
Socket.listen("connect", () => {
|
|
201
|
+
for (const target of socketStatusElements) {
|
|
202
|
+
setSocketStatus(target, "connected");
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
Socket.listen("disconnect", () => {
|
|
206
|
+
for (const target of socketStatusElements) {
|
|
207
|
+
setSocketStatus(target, "disconnected");
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
socketStatusListenersInitialized = true;
|
|
211
|
+
}
|
|
212
|
+
if (Socket.connected) setSocketStatus(element, "connected");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function initializeSocketStatus(root = document) {
|
|
216
|
+
if (root.matches?.(socketStatusSelector)) registerSocketStatus(root);
|
|
217
|
+
root.querySelectorAll?.(socketStatusSelector).forEach(registerSocketStatus);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function removeSocketStatus(root) {
|
|
221
|
+
if (root.matches?.(socketStatusSelector)) socketStatusElements.delete(root);
|
|
222
|
+
root.querySelectorAll?.(socketStatusSelector).forEach(element => {
|
|
223
|
+
socketStatusElements.delete(element);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function registerSocketOn(element) {
|
|
228
|
+
if (!element?.matches?.(socketOnSelector)) return;
|
|
229
|
+
|
|
230
|
+
const previousMethod = socketOnElements.get(element);
|
|
231
|
+
const method = element.getAttribute("socket-on")?.trim();
|
|
232
|
+
if (previousMethod && previousMethod !== method) {
|
|
233
|
+
socketOnListeners.get(previousMethod)?.elements.delete(element);
|
|
234
|
+
socketOnElements.delete(element);
|
|
235
|
+
}
|
|
236
|
+
if (!method || socketOnElements.has(element)) return;
|
|
237
|
+
|
|
238
|
+
let listener = socketOnListeners.get(method);
|
|
239
|
+
if (!listener) {
|
|
240
|
+
listener = { elements: new Set() };
|
|
241
|
+
listener.callback = (data) => {
|
|
242
|
+
for (const target of listener.elements) {
|
|
243
|
+
if (target.getAttribute("socket-on") !== method) continue;
|
|
244
|
+
setSocketOnValue(target, data).catch((error) => {
|
|
245
|
+
console.error("CoCreate: unable to set socket listener value.", error);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
socketOnListeners.set(method, listener);
|
|
250
|
+
Socket.listen(method, listener.callback);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
listener.elements.add(element);
|
|
254
|
+
socketOnElements.set(element, method);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function initializeSocketOn(root = document) {
|
|
258
|
+
if (root.matches?.(socketOnSelector)) registerSocketOn(root);
|
|
259
|
+
root.querySelectorAll?.(socketOnSelector).forEach(registerSocketOn);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function removeSocketOn(root) {
|
|
263
|
+
const elements = [];
|
|
264
|
+
if (root.matches?.(socketOnSelector)) elements.push(root);
|
|
265
|
+
elements.push(...root.querySelectorAll?.(socketOnSelector) || []);
|
|
266
|
+
for (const element of elements) {
|
|
267
|
+
const method = socketOnElements.get(element);
|
|
268
|
+
if (method) socketOnListeners.get(method)?.elements.delete(element);
|
|
269
|
+
socketOnElements.delete(element);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
Actions.init({ name: "send", callback: send });
|
|
274
|
+
|
|
275
|
+
initializeSocketOn();
|
|
276
|
+
initializeSocketStatus();
|
|
277
|
+
|
|
278
|
+
Observer.init({
|
|
279
|
+
name: "CoCreateElementsSocketOnAdded",
|
|
280
|
+
types: ["addedNodes"],
|
|
281
|
+
selector: socketOnSelector,
|
|
282
|
+
callback: (mutation) => initializeSocketOn(mutation.target)
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
Observer.init({
|
|
286
|
+
name: "CoCreateElementsSocketOnRemoved",
|
|
287
|
+
types: ["removedNodes"],
|
|
288
|
+
selector: socketOnSelector,
|
|
289
|
+
callback: (mutation) => removeSocketOn(mutation.target)
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
Observer.init({
|
|
293
|
+
name: "CoCreateElementsSocketOnAttribute",
|
|
294
|
+
types: ["attributes"],
|
|
295
|
+
attributeFilter: ["socket-on"],
|
|
296
|
+
selector: socketOnSelector,
|
|
297
|
+
callback: (mutation) => registerSocketOn(mutation.target)
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
Observer.init({
|
|
301
|
+
name: "CoCreateElementsSocketStatusAdded",
|
|
302
|
+
types: ["addedNodes"],
|
|
303
|
+
selector: socketStatusSelector,
|
|
304
|
+
callback: (mutation) => initializeSocketStatus(mutation.target)
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
Observer.init({
|
|
308
|
+
name: "CoCreateElementsSocketStatusRemoved",
|
|
309
|
+
types: ["removedNodes"],
|
|
310
|
+
selector: socketStatusSelector,
|
|
311
|
+
callback: (mutation) => removeSocketStatus(mutation.target)
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
export { getData as getSocketData, send as sendSocketMessage };
|
|
315
|
+
export {
|
|
316
|
+
initializeSocketOn,
|
|
317
|
+
registerSocketOn,
|
|
318
|
+
removeSocketOn,
|
|
319
|
+
setSocketOnValue
|
|
320
|
+
};
|
|
321
|
+
export default {
|
|
322
|
+
getData,
|
|
323
|
+
initializeSocketOn,
|
|
324
|
+
registerSocketOn,
|
|
325
|
+
removeSocketOn,
|
|
326
|
+
send,
|
|
327
|
+
setSocketOnValue
|
|
328
|
+
};
|
package/src/srcHandler.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import observer from "@cocreate/observer";
|
|
2
2
|
import { getRelativePath, ObjectId } from "@cocreate/utils";
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
"
|
|
4
|
+
const sourceTagNames = new Set([
|
|
5
|
+
"IMG",
|
|
6
|
+
"VIDEO",
|
|
7
|
+
"AUDIO",
|
|
8
|
+
"SCRIPT",
|
|
9
|
+
"INPUT",
|
|
10
|
+
"IFRAME",
|
|
11
|
+
"FRAME",
|
|
12
|
+
"LINK",
|
|
13
|
+
"SOURCE"
|
|
14
|
+
]);
|
|
15
|
+
const selector = "[src], [source], [stream]";
|
|
6
16
|
|
|
7
17
|
const initializing = new WeakMap();
|
|
8
18
|
|
|
@@ -14,9 +24,19 @@ const initializing = new WeakMap();
|
|
|
14
24
|
*/
|
|
15
25
|
function isSameOrigin(urlStr) {
|
|
16
26
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
let origin = window.location.origin;
|
|
28
|
+
|
|
29
|
+
// Sandboxed srcdoc/about:blank documents have an opaque URL even when
|
|
30
|
+
// same-origin access was explicitly granted. In that case, parent
|
|
31
|
+
// access succeeds only when the browser considers the frame trusted.
|
|
32
|
+
if (origin === "null" && window.parent !== window) {
|
|
33
|
+
origin = window.parent.location.origin;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!origin || origin === "null") return false;
|
|
37
|
+
|
|
38
|
+
const url = new URL(urlStr, origin);
|
|
39
|
+
return url.origin === origin;
|
|
20
40
|
} catch (err) {
|
|
21
41
|
console.error("Invalid URL format:", urlStr, err);
|
|
22
42
|
return false;
|
|
@@ -24,6 +44,8 @@ function isSameOrigin(urlStr) {
|
|
|
24
44
|
}
|
|
25
45
|
|
|
26
46
|
function init(elements) {
|
|
47
|
+
if (elements?.tagName && sourceTagNames.has(elements.tagName)) return;
|
|
48
|
+
|
|
27
49
|
if (elements && !Array.isArray(elements)) elements = [elements];
|
|
28
50
|
|
|
29
51
|
if (!elements) elements = document.querySelectorAll(selector);
|
|
@@ -33,6 +55,8 @@ function init(elements) {
|
|
|
33
55
|
|
|
34
56
|
async function initElements(elements) {
|
|
35
57
|
for (let element of elements) {
|
|
58
|
+
if (sourceTagNames.has(element.tagName)) continue;
|
|
59
|
+
|
|
36
60
|
let src =
|
|
37
61
|
element.getAttribute("src") ||
|
|
38
62
|
element.getAttribute("source") ||
|
|
@@ -41,27 +65,45 @@ async function initElements(elements) {
|
|
|
41
65
|
if (!src || /{{\s*([\w\W]+)\s*}}/g.test(src)) continue;
|
|
42
66
|
|
|
43
67
|
let initialize = initializing.get(element);
|
|
68
|
+
|
|
69
|
+
if (element.hasAttribute("rendered")) {
|
|
70
|
+
element.removeAttribute("rendered");
|
|
71
|
+
initializing.set(element, { src });
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
44
74
|
if (initialize && initialize.src === src) continue;
|
|
45
75
|
|
|
46
|
-
|
|
76
|
+
|
|
77
|
+
const request = { src };
|
|
78
|
+
initializing.set(element, request);
|
|
79
|
+
const isCurrentRequest = () =>
|
|
80
|
+
initializing.get(element) === request &&
|
|
81
|
+
(element.getAttribute("src") ||
|
|
82
|
+
element.getAttribute("source") ||
|
|
83
|
+
element.getAttribute("stream")) === src;
|
|
47
84
|
try {
|
|
48
85
|
// --- SECURITY FIX: Conditional Sensitive Headers ---
|
|
49
86
|
// Only attach the local state header if fetching from the same origin.
|
|
50
87
|
let headers = {};
|
|
51
88
|
if (isSameOrigin(src)) {
|
|
52
|
-
let state =
|
|
89
|
+
let state = sessionStorage.getItem("state");
|
|
53
90
|
if (state) {
|
|
54
91
|
headers["X-State"] = state;
|
|
55
92
|
}
|
|
56
93
|
}
|
|
57
94
|
|
|
58
95
|
let response = await fetch(src, { headers });
|
|
96
|
+
if (!isCurrentRequest()) continue;
|
|
59
97
|
|
|
60
98
|
if (response.status === 404 || response.status === 403) {
|
|
61
99
|
element.setAttribute("error", response.status);
|
|
62
100
|
continue;
|
|
63
101
|
}
|
|
64
102
|
|
|
103
|
+
if (response.ok && ["304", "403", "404"].includes(element.getAttribute("error"))) {
|
|
104
|
+
element.removeAttribute("error");
|
|
105
|
+
}
|
|
106
|
+
|
|
65
107
|
if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
|
|
66
108
|
// Media streams can safely load cross-origin without risk of DOM-based XSS
|
|
67
109
|
initMediaSource(element, src);
|
|
@@ -74,24 +116,22 @@ async function initElements(elements) {
|
|
|
74
116
|
continue;
|
|
75
117
|
}
|
|
76
118
|
|
|
77
|
-
if (element.hasAttribute("rendered")) {
|
|
78
|
-
element.removeAttribute("rendered");
|
|
79
|
-
}
|
|
80
119
|
let text = await response.text();
|
|
120
|
+
if (!isCurrentRequest()) continue;
|
|
81
121
|
if (text) {
|
|
82
|
-
let path = "";
|
|
83
|
-
let pathElement = element.closest("[path]");
|
|
84
|
-
if (pathElement) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
} else {
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
// Replace $relativePath in the fetched text
|
|
92
|
-
if (path) {
|
|
93
|
-
|
|
94
|
-
}
|
|
122
|
+
// let path = "";
|
|
123
|
+
// let pathElement = element.closest("[path]");
|
|
124
|
+
// if (pathElement) {
|
|
125
|
+
// path =
|
|
126
|
+
// pathElement.getAttribute("path") ||
|
|
127
|
+
// getRelativePath();
|
|
128
|
+
// } else {
|
|
129
|
+
// path = getRelativePath();
|
|
130
|
+
// }
|
|
131
|
+
// // Replace $relativePath in the fetched text
|
|
132
|
+
// if (path) {
|
|
133
|
+
// text = text.replaceAll(/\$relativePath\/?/g, path);
|
|
134
|
+
// }
|
|
95
135
|
// Replace ObjectId() with a new ObjectId
|
|
96
136
|
text = text.replaceAll("ObjectId()", () => {
|
|
97
137
|
return ObjectId().toString();
|
|
@@ -278,8 +318,9 @@ function base64ToArrayBuffer(base64) {
|
|
|
278
318
|
observer.init({
|
|
279
319
|
name: "CoCreateSrc",
|
|
280
320
|
types: ["addedNodes"],
|
|
281
|
-
selector:
|
|
321
|
+
selector: "[src], [source], [stream]",
|
|
282
322
|
callback: function (mutation) {
|
|
323
|
+
if (mutation.move) return;
|
|
283
324
|
init(mutation.target);
|
|
284
325
|
}
|
|
285
326
|
});
|
|
@@ -300,15 +341,10 @@ observer.init({
|
|
|
300
341
|
name: "CoCreateSrcAttributes",
|
|
301
342
|
types: ["attributes"],
|
|
302
343
|
attributeFilter: ["src"],
|
|
303
|
-
selector:
|
|
344
|
+
selector: "[src], [source], [stream]",
|
|
304
345
|
callback: function (mutation) {
|
|
305
|
-
|
|
306
|
-
mutation.oldValue !==
|
|
307
|
-
mutation.target.getAttribute(mutation.attributeName)
|
|
308
|
-
) {
|
|
309
|
-
init(mutation.target);
|
|
310
|
-
}
|
|
346
|
+
init(mutation.target);
|
|
311
347
|
}
|
|
312
348
|
});
|
|
313
349
|
|
|
314
|
-
init();
|
|
350
|
+
init();
|
package/src/subscribeHandler.js
CHANGED
|
@@ -2,102 +2,102 @@ import socket from "@cocreate/socket-client";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Canonical scope attributes included in socket server subscription channel keys.
|
|
5
|
-
* Excludes field-level "key" and "
|
|
5
|
+
* Excludes field-level "key" and "filter" so document and collection channels remain cohesive.
|
|
6
6
|
*/
|
|
7
7
|
const SUBSCRIPTION_ATTRIBUTES = [
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"namespace",
|
|
9
|
+
"room",
|
|
10
10
|
"storage",
|
|
11
11
|
"database",
|
|
12
12
|
"array",
|
|
13
|
-
"index",
|
|
14
13
|
"object"
|
|
15
14
|
];
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
export function getSubscription(data) {
|
|
17
|
+
const object = {};
|
|
18
|
+
|
|
19
|
+
for (const attribute of SUBSCRIPTION_ATTRIBUTES) {
|
|
20
|
+
let value = data[attribute];
|
|
21
|
+
|
|
22
|
+
if (attribute === "object") {
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
value = value
|
|
25
|
+
.map(item => typeof item === "object" ? item?._id : item)
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.sort();
|
|
28
|
+
} else if (typeof value === "object" && value !== null) {
|
|
29
|
+
value = value._id;
|
|
30
|
+
}
|
|
31
|
+
} else if (Array.isArray(value)) {
|
|
32
|
+
value = [...value].sort();
|
|
33
|
+
}
|
|
30
34
|
|
|
31
|
-
for (const attr of SUBSCRIPTION_ATTRIBUTES) {
|
|
32
|
-
const value = rawObj[attr];
|
|
33
35
|
if (value !== undefined && value !== null && value !== "") {
|
|
34
|
-
|
|
36
|
+
object[attribute] = value;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
// 1. Specific Document Key (includes exact object ID if present)
|
|
41
|
-
const sortedSpecific = Object.fromEntries(
|
|
42
|
-
Object.entries(baseSubObj).sort(([a], [b]) => a.localeCompare(b))
|
|
40
|
+
const normalizedObject = Object.fromEntries(
|
|
41
|
+
Object.entries(object).sort(([a], [b]) => a.localeCompare(b))
|
|
43
42
|
);
|
|
44
|
-
keysSet.add(JSON.stringify(sortedSpecific));
|
|
45
|
-
|
|
46
|
-
// 2. Generic Scope Key (targets list component subscribers watching the collection)
|
|
47
|
-
if (baseSubObj.object) {
|
|
48
|
-
const genericSubObj = { ...baseSubObj };
|
|
49
|
-
genericSubObj.object = []; // Generalize object scope
|
|
50
43
|
|
|
51
|
-
|
|
52
|
-
Object.entries(genericSubObj).sort(([a], [b]) => a.localeCompare(b))
|
|
53
|
-
);
|
|
54
|
-
keysSet.add(JSON.stringify(sortedGeneric));
|
|
55
|
-
}
|
|
44
|
+
if (normalizedObject.object && !normalizedObject.array) return;
|
|
56
45
|
|
|
57
|
-
return
|
|
46
|
+
return {
|
|
47
|
+
string: JSON.stringify(normalizedObject),
|
|
48
|
+
object: normalizedObject
|
|
49
|
+
};
|
|
58
50
|
}
|
|
59
51
|
|
|
60
52
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* @
|
|
53
|
+
* Derives all target subscription channel keys for write operations (create, update, delete).
|
|
54
|
+
* Returns an array containing both the specific document scope key (with ID)
|
|
55
|
+
* and the generic scope key (without ID) to notify both single-item and list subscribers.
|
|
56
|
+
*
|
|
57
|
+
* @param {Object} data - CRUD data object
|
|
58
|
+
* @returns {string[]} Array of JSON-stringified subscription key channel strings
|
|
66
59
|
*/
|
|
67
|
-
export function
|
|
68
|
-
if (!
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
60
|
+
export function deriveSubscriptionKeys(data) {
|
|
61
|
+
if (!data) return [];
|
|
62
|
+
const subscription = getSubscription(data);
|
|
63
|
+
if (!subscription) return [];
|
|
64
|
+
const baseSubObj = { ...subscription.object };
|
|
65
|
+
const objects = Array.isArray(baseSubObj.object)
|
|
66
|
+
? baseSubObj.object
|
|
67
|
+
: baseSubObj.object
|
|
68
|
+
? [baseSubObj.object]
|
|
69
|
+
: [];
|
|
70
|
+
delete baseSubObj.object;
|
|
71
|
+
const keysSet = new Set();
|
|
72
|
+
const addKey = (scope) => {
|
|
73
|
+
const sortedScope = Object.fromEntries(
|
|
74
|
+
Object.entries(scope).sort(([a], [b]) => a.localeCompare(b))
|
|
75
|
+
);
|
|
76
|
+
keysSet.add(JSON.stringify(sortedScope));
|
|
76
77
|
};
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
79
|
+
for (const object of objects)
|
|
80
|
+
addKey({ ...baseSubObj, object });
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
// Collection subscribers use the same scope with the object omitted.
|
|
83
|
+
addKey(baseSubObj);
|
|
84
|
+
|
|
85
|
+
return Array.from(keysSet);
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
/**
|
|
86
89
|
* Sends an unsubscription payload to the socket server when an element scope is unmounted.
|
|
87
|
-
*
|
|
90
|
+
*
|
|
88
91
|
* @param {Object} dataKey - Generated dataKey object { string, object }
|
|
89
92
|
* @param {Object} [data] - Optional metadata or payload overrides
|
|
90
93
|
* @returns {Object|undefined} Payload sent to the socket
|
|
91
94
|
*/
|
|
92
|
-
export function unsubscribe(
|
|
93
|
-
if (!
|
|
95
|
+
export function unsubscribe(subscription) {
|
|
96
|
+
if (!subscription || !subscription.string) return;
|
|
94
97
|
|
|
95
98
|
const payload = {
|
|
96
99
|
method: "unsubscribe",
|
|
97
|
-
|
|
98
|
-
subscriptionKeys: deriveSubscriptionKeys(dataKey.object || dataKey),
|
|
99
|
-
...dataKey.object,
|
|
100
|
-
...data
|
|
100
|
+
subscription: subscription.string
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
if (socket && typeof socket.send === "function") {
|
|
@@ -108,7 +108,7 @@ export function unsubscribe(dataKey, data = {}) {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
export default {
|
|
111
|
+
getSubscription,
|
|
111
112
|
deriveSubscriptionKeys,
|
|
112
|
-
subscribe,
|
|
113
113
|
unsubscribe
|
|
114
|
-
};
|
|
114
|
+
};
|