dolphin-client 1.0.5 → 1.0.9
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/.vscode/dolphin-tags.json +558 -0
- package/README.md +49 -2
- package/bin/cli.cjs +60 -0
- package/dist/api.d.ts +20 -0
- package/dist/core.d.ts +3 -1
- package/dist/dolphin-client.js +429 -25
- package/dist/dolphin-client.min.js +28 -17
- package/dist/index.cjs +429 -25
- package/dist/index.js +429 -25
- package/dist/store.d.ts +7 -0
- package/fulltutorial.md +235 -0
- package/package.json +10 -3
- package/scripts/postinstall.js +57 -0
package/dist/index.cjs
CHANGED
|
@@ -47,7 +47,21 @@ var APIHandler = class {
|
|
|
47
47
|
target.requestDirect = (method, path, body, options) => {
|
|
48
48
|
return this.requestDirect(method, path, body, options);
|
|
49
49
|
};
|
|
50
|
-
|
|
50
|
+
target._findCSRFToken = () => this._findCSRFToken();
|
|
51
|
+
target._resolveBaseUrl = (path) => this._resolveBaseUrl(path);
|
|
52
|
+
target._normalizeValidationErrors = (errData) => this._normalizeValidationErrors(errData);
|
|
53
|
+
const methods = [
|
|
54
|
+
"get",
|
|
55
|
+
"post",
|
|
56
|
+
"put",
|
|
57
|
+
"patch",
|
|
58
|
+
"del",
|
|
59
|
+
"request",
|
|
60
|
+
"requestDirect",
|
|
61
|
+
"_findCSRFToken",
|
|
62
|
+
"_resolveBaseUrl",
|
|
63
|
+
"_normalizeValidationErrors"
|
|
64
|
+
];
|
|
51
65
|
return new Proxy(target, {
|
|
52
66
|
get: (t, prop) => {
|
|
53
67
|
if (typeof prop === "string" && !methods.includes(prop)) {
|
|
@@ -57,6 +71,108 @@ var APIHandler = class {
|
|
|
57
71
|
}
|
|
58
72
|
});
|
|
59
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Attempts to find a CSRF token in the document (meta tags, forms, or cookies).
|
|
76
|
+
* Works for Laravel, CakePHP, WordPress, Express, NestJS, etc.
|
|
77
|
+
* @private
|
|
78
|
+
*/
|
|
79
|
+
_findCSRFToken() {
|
|
80
|
+
if (typeof document === "undefined") return null;
|
|
81
|
+
const metaNames = ["csrf-token", "_csrf", "xsrf-token", "csrf_token"];
|
|
82
|
+
for (const name of metaNames) {
|
|
83
|
+
const metaEl = document.querySelector(`meta[name="${name}"], meta[content][name$="${name}"]`);
|
|
84
|
+
if (metaEl) {
|
|
85
|
+
const token = metaEl.getAttribute("content");
|
|
86
|
+
if (token) return token;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const inputNames = ["_csrfToken", "_token", "_csrf", "csrf_token"];
|
|
90
|
+
for (const name of inputNames) {
|
|
91
|
+
const inputEl = document.querySelector(`input[type="hidden"][name="${name}"]`);
|
|
92
|
+
if (inputEl && inputEl.value) return inputEl.value;
|
|
93
|
+
}
|
|
94
|
+
const cookies = ["csrfToken", "XSRF-TOKEN", "_csrf", "csrf_token"];
|
|
95
|
+
for (const name of cookies) {
|
|
96
|
+
const match = document.cookie.match(new RegExp("(^|;\\s*)" + name + "=([^;]*)"));
|
|
97
|
+
if (match) return decodeURIComponent(match[2]);
|
|
98
|
+
}
|
|
99
|
+
const wpNonce = typeof window !== "undefined" && window.wpApiSettings?.nonce;
|
|
100
|
+
if (wpNonce) return wpNonce;
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Dynamically resolves the Base Path/URL from `<base href="...">` or subfolders.
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
_resolveBaseUrl(path) {
|
|
108
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
109
|
+
return path;
|
|
110
|
+
}
|
|
111
|
+
let baseUrl = this.client.httpUrl;
|
|
112
|
+
if (typeof document !== "undefined") {
|
|
113
|
+
const baseEl = document.querySelector("base[href]");
|
|
114
|
+
if (baseEl) {
|
|
115
|
+
const href = baseEl.getAttribute("href") || "";
|
|
116
|
+
if (href && href !== "/") {
|
|
117
|
+
const cleanHref = href.endsWith("/") ? href.slice(0, -1) : href;
|
|
118
|
+
baseUrl = `${this.client.httpUrl}${cleanHref.startsWith("/") ? cleanHref : "/" + cleanHref}`;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
const metaBase = document.querySelector('meta[name="base-path"]');
|
|
122
|
+
if (metaBase) {
|
|
123
|
+
const content = metaBase.getAttribute("content") || "";
|
|
124
|
+
if (content && content !== "/") {
|
|
125
|
+
const cleanContent = content.endsWith("/") ? content.slice(0, -1) : content;
|
|
126
|
+
baseUrl = `${this.client.httpUrl}${cleanContent.startsWith("/") ? cleanContent : "/" + cleanContent}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const cleanPath = path.startsWith("/") ? path : "/" + path;
|
|
132
|
+
return `${baseUrl}${cleanPath}`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Normalizes backend validation errors from major PHP and Node.js frameworks
|
|
136
|
+
* into a unified { [field]: message } object.
|
|
137
|
+
* @private
|
|
138
|
+
*/
|
|
139
|
+
_normalizeValidationErrors(errData) {
|
|
140
|
+
const normalized = {};
|
|
141
|
+
if (!errData || typeof errData !== "object") return normalized;
|
|
142
|
+
const errors = errData.errors || errData.validationErrors || errData;
|
|
143
|
+
if (Array.isArray(errors)) {
|
|
144
|
+
for (const err of errors) {
|
|
145
|
+
if (err && typeof err === "object") {
|
|
146
|
+
const field = err.path || err.param || err.field || err.property;
|
|
147
|
+
const msg = err.msg || err.message || err.error;
|
|
148
|
+
if (field && msg) {
|
|
149
|
+
normalized[field] = Array.isArray(msg) ? msg[0] : msg;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return normalized;
|
|
154
|
+
}
|
|
155
|
+
if (typeof errors === "object" && errors !== null) {
|
|
156
|
+
for (const key in errors) {
|
|
157
|
+
const val = errors[key];
|
|
158
|
+
if (!val) continue;
|
|
159
|
+
if (Array.isArray(val)) {
|
|
160
|
+
if (val.length > 0) {
|
|
161
|
+
normalized[key] = String(val[0]);
|
|
162
|
+
}
|
|
163
|
+
} else if (typeof val === "object") {
|
|
164
|
+
const innerKeys = Object.keys(val);
|
|
165
|
+
if (innerKeys.length > 0) {
|
|
166
|
+
const firstInnerKey = innerKeys[0];
|
|
167
|
+
normalized[key] = String(val[firstInnerKey]);
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
normalized[key] = String(val);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return normalized;
|
|
175
|
+
}
|
|
60
176
|
/**
|
|
61
177
|
* Intercept request for offline-first caching and queuing.
|
|
62
178
|
*/
|
|
@@ -107,30 +223,68 @@ var APIHandler = class {
|
|
|
107
223
|
*/
|
|
108
224
|
async requestDirect(method, path, body = null, options = {}) {
|
|
109
225
|
const _isRetry = options._isRetry === true;
|
|
110
|
-
|
|
226
|
+
let finalMethod = method.toUpperCase();
|
|
227
|
+
let finalBody = body;
|
|
228
|
+
const headers = {
|
|
229
|
+
"Content-Type": "application/json",
|
|
230
|
+
...options.headers || {}
|
|
231
|
+
};
|
|
232
|
+
if (["PUT", "PATCH", "DELETE"].includes(finalMethod)) {
|
|
233
|
+
if (this.client.options.methodSpoofing || options.methodSpoofing) {
|
|
234
|
+
headers["X-HTTP-Method-Override"] = finalMethod;
|
|
235
|
+
if (finalBody instanceof FormData) {
|
|
236
|
+
finalBody.append("_method", finalMethod);
|
|
237
|
+
} else if (finalBody && typeof finalBody === "object") {
|
|
238
|
+
finalBody = {
|
|
239
|
+
...finalBody,
|
|
240
|
+
_method: finalMethod
|
|
241
|
+
};
|
|
242
|
+
} else if (!finalBody) {
|
|
243
|
+
finalBody = { _method: finalMethod };
|
|
244
|
+
}
|
|
245
|
+
finalMethod = "POST";
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const url = this._resolveBaseUrl(path);
|
|
111
249
|
if (this.client.options.debug) {
|
|
112
|
-
console.log(`%c\u{1F680} [Dolphin API Request]:`, "color: #3b82f6; font-weight: bold;", method.toUpperCase(), path,
|
|
250
|
+
console.log(`%c\u{1F680} [Dolphin API Request]:`, "color: #3b82f6; font-weight: bold;", method.toUpperCase(), path, finalBody || "");
|
|
113
251
|
}
|
|
114
252
|
const controller = new AbortController();
|
|
115
253
|
const timeoutId = setTimeout(
|
|
116
254
|
() => controller.abort(),
|
|
117
255
|
this.client.options.timeout
|
|
118
256
|
);
|
|
119
|
-
const headers = {
|
|
120
|
-
"Content-Type": "application/json",
|
|
121
|
-
...options.headers || {}
|
|
122
|
-
};
|
|
123
257
|
if (this.client.accessToken) {
|
|
124
258
|
headers["Authorization"] = `Bearer ${this.client.accessToken}`;
|
|
125
259
|
}
|
|
260
|
+
if (["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase())) {
|
|
261
|
+
const csrfToken = this._findCSRFToken();
|
|
262
|
+
if (csrfToken) {
|
|
263
|
+
headers["X-CSRF-Token"] = csrfToken;
|
|
264
|
+
headers["X-XSRF-TOKEN"] = csrfToken;
|
|
265
|
+
headers["X-CSRFToken"] = csrfToken;
|
|
266
|
+
headers["X-WP-Nonce"] = csrfToken;
|
|
267
|
+
if (finalBody && typeof finalBody === "object") {
|
|
268
|
+
if (!finalBody._csrfToken && !finalBody._token && !finalBody._csrf) {
|
|
269
|
+
finalBody = {
|
|
270
|
+
...finalBody,
|
|
271
|
+
_csrfToken: csrfToken,
|
|
272
|
+
_token: csrfToken,
|
|
273
|
+
_csrf: csrfToken
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
126
279
|
const fetchOptions = { ...options };
|
|
127
280
|
delete fetchOptions._isRetry;
|
|
281
|
+
delete fetchOptions.methodSpoofing;
|
|
128
282
|
try {
|
|
129
283
|
const response = await fetch(url, {
|
|
130
|
-
method,
|
|
284
|
+
method: finalMethod,
|
|
131
285
|
headers,
|
|
132
286
|
signal: controller.signal,
|
|
133
|
-
...
|
|
287
|
+
...finalBody ? { body: JSON.stringify(finalBody) } : {},
|
|
134
288
|
...fetchOptions
|
|
135
289
|
});
|
|
136
290
|
clearTimeout(timeoutId);
|
|
@@ -162,6 +316,14 @@ var APIHandler = class {
|
|
|
162
316
|
if (this.client.options.debug) {
|
|
163
317
|
console.error(`%c\u274C [Dolphin API Error]:`, "color: #ef4444; font-weight: bold;", method.toUpperCase(), path, err);
|
|
164
318
|
}
|
|
319
|
+
if (err && typeof err === "object" && err.data) {
|
|
320
|
+
const normErrors = this._normalizeValidationErrors(err.data);
|
|
321
|
+
if (Object.keys(normErrors).length > 0) {
|
|
322
|
+
for (const field in normErrors) {
|
|
323
|
+
this.client.publish(`errors/${field}`, normErrors[field]);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
165
327
|
if (err.name === "AbortError") {
|
|
166
328
|
throw { status: 408, data: { error: "Request timed out" } };
|
|
167
329
|
}
|
|
@@ -295,12 +457,15 @@ var DolphinStore = class {
|
|
|
295
457
|
data;
|
|
296
458
|
listeners;
|
|
297
459
|
subscribed;
|
|
460
|
+
/** @fix: Store unsubscribe functions so destroy() can clean up WS subscriptions (was: subscriptions never removed) */
|
|
461
|
+
_unsubscribers;
|
|
298
462
|
/** @param {DolphinClient} client */
|
|
299
463
|
constructor(client) {
|
|
300
464
|
this.client = client;
|
|
301
465
|
this.data = /* @__PURE__ */ new Map();
|
|
302
466
|
this.listeners = /* @__PURE__ */ new Set();
|
|
303
467
|
this.subscribed = /* @__PURE__ */ new Set();
|
|
468
|
+
this._unsubscribers = /* @__PURE__ */ new Map();
|
|
304
469
|
return new Proxy(this, {
|
|
305
470
|
get: (target, prop) => {
|
|
306
471
|
if (prop in target) return target[prop];
|
|
@@ -352,9 +517,12 @@ var DolphinStore = class {
|
|
|
352
517
|
state.error = null;
|
|
353
518
|
this._applyTransform(state);
|
|
354
519
|
if (!this.subscribed.has(name)) {
|
|
355
|
-
this.client.
|
|
520
|
+
const unsubscribe = () => this.client.unsubscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
|
|
521
|
+
const updateHandler = (update) => {
|
|
356
522
|
this._handleRemoteUpdate(name, update);
|
|
357
|
-
}
|
|
523
|
+
};
|
|
524
|
+
this.client.subscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
|
|
525
|
+
this._unsubscribers.set(name, unsubscribe);
|
|
358
526
|
this.subscribed.add(name);
|
|
359
527
|
}
|
|
360
528
|
} catch (e) {
|
|
@@ -411,6 +579,22 @@ var DolphinStore = class {
|
|
|
411
579
|
_notify() {
|
|
412
580
|
this.listeners.forEach((l) => l());
|
|
413
581
|
}
|
|
582
|
+
/**
|
|
583
|
+
* Clean up all WebSocket subscriptions and listeners.
|
|
584
|
+
* Call this when the store is no longer needed to prevent resource leaks.
|
|
585
|
+
*/
|
|
586
|
+
destroy() {
|
|
587
|
+
this._unsubscribers.forEach((unsub) => {
|
|
588
|
+
try {
|
|
589
|
+
unsub();
|
|
590
|
+
} catch {
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
this._unsubscribers.clear();
|
|
594
|
+
this.subscribed.clear();
|
|
595
|
+
this.listeners.clear();
|
|
596
|
+
this.data.clear();
|
|
597
|
+
}
|
|
414
598
|
};
|
|
415
599
|
|
|
416
600
|
// src/core.ts
|
|
@@ -430,6 +614,8 @@ var DolphinClient = class {
|
|
|
430
614
|
fileHandlers;
|
|
431
615
|
_offlineQueue;
|
|
432
616
|
reconnectAttempts;
|
|
617
|
+
/** @fix: Store timer ID so disconnect() can cancel pending reconnects (was: memory/logic leak) */
|
|
618
|
+
_reconnectTimer;
|
|
433
619
|
_attachedListeners;
|
|
434
620
|
constructor(url = "", deviceId = "", options = {}) {
|
|
435
621
|
if (!url && typeof window !== "undefined") url = window.location.host;
|
|
@@ -439,7 +625,7 @@ var DolphinClient = class {
|
|
|
439
625
|
else if (typeof window !== "undefined") protocol = window.location.protocol;
|
|
440
626
|
this.host = (url || "localhost").replace(/\/$/, "").replace(/^https?:\/\//, "");
|
|
441
627
|
this.httpUrl = `${protocol}//${this.host}`;
|
|
442
|
-
this.deviceId = deviceId || "web_" + Math.random().toString(36).
|
|
628
|
+
this.deviceId = deviceId || (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? "web_" + crypto.randomUUID().replace(/-/g, "").slice(0, 8) : "web_" + Math.random().toString(36).slice(2, 10));
|
|
443
629
|
this.options = {
|
|
444
630
|
timeout: 15e3,
|
|
445
631
|
chunkSize: 65536,
|
|
@@ -447,6 +633,9 @@ var DolphinClient = class {
|
|
|
447
633
|
maxReconnect: 5,
|
|
448
634
|
autoRefreshToken: true,
|
|
449
635
|
debug: false,
|
|
636
|
+
methodSpoofing: false,
|
|
637
|
+
routerViewport: "main, #viewport, body",
|
|
638
|
+
routerTransitions: true,
|
|
450
639
|
...options
|
|
451
640
|
};
|
|
452
641
|
this.socket = null;
|
|
@@ -466,6 +655,7 @@ var DolphinClient = class {
|
|
|
466
655
|
this.fileHandlers = /* @__PURE__ */ new Set();
|
|
467
656
|
this._offlineQueue = [];
|
|
468
657
|
this.reconnectAttempts = 0;
|
|
658
|
+
this._reconnectTimer = null;
|
|
469
659
|
this._attachedListeners = [];
|
|
470
660
|
if (typeof window !== "undefined" && typeof this._initDOMBinding === "function") {
|
|
471
661
|
this._initDOMBinding();
|
|
@@ -494,6 +684,9 @@ var DolphinClient = class {
|
|
|
494
684
|
// ── WebSocket ─────────────────────────────────────────────────────────────
|
|
495
685
|
/** Connect to the Dolphin realtime server */
|
|
496
686
|
async connect() {
|
|
687
|
+
if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) {
|
|
688
|
+
return Promise.resolve();
|
|
689
|
+
}
|
|
497
690
|
return new Promise((resolve, reject) => {
|
|
498
691
|
const protocol = this.httpUrl.startsWith("https") ? "wss:" : "ws:";
|
|
499
692
|
const wsUrl = `${protocol}//${this.host}/realtime?deviceId=${this.deviceId}`;
|
|
@@ -518,11 +711,18 @@ var DolphinClient = class {
|
|
|
518
711
|
}
|
|
519
712
|
/** Disconnect cleanly */
|
|
520
713
|
disconnect() {
|
|
714
|
+
if (this._reconnectTimer !== null) {
|
|
715
|
+
clearTimeout(this._reconnectTimer);
|
|
716
|
+
this._reconnectTimer = null;
|
|
717
|
+
}
|
|
521
718
|
if (this.socket) {
|
|
522
719
|
this.socket.onclose = null;
|
|
523
720
|
this.socket.close();
|
|
524
721
|
this.socket = null;
|
|
525
722
|
}
|
|
723
|
+
if (typeof this._collabCleanup === "function") {
|
|
724
|
+
this._collabCleanup();
|
|
725
|
+
}
|
|
526
726
|
this.cleanupDomListeners();
|
|
527
727
|
}
|
|
528
728
|
/** @private */
|
|
@@ -610,8 +810,11 @@ var DolphinClient = class {
|
|
|
610
810
|
this.reconnectAttempts++;
|
|
611
811
|
const delay = Math.pow(2, this.reconnectAttempts) * 1e3;
|
|
612
812
|
console.log(`[Dolphin] Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts})...`);
|
|
613
|
-
|
|
614
|
-
|
|
813
|
+
this._reconnectTimer = setTimeout(() => {
|
|
814
|
+
this._reconnectTimer = null;
|
|
815
|
+
this.connect().catch(() => {
|
|
816
|
+
});
|
|
817
|
+
}, delay);
|
|
615
818
|
} else {
|
|
616
819
|
console.error("[Dolphin] Max reconnect attempts reached.");
|
|
617
820
|
}
|
|
@@ -789,6 +992,7 @@ var DolphinClient = class {
|
|
|
789
992
|
|
|
790
993
|
// src/dom.ts
|
|
791
994
|
function attachDOMBinding(clientProto) {
|
|
995
|
+
const componentPromiseCache = /* @__PURE__ */ new Map();
|
|
792
996
|
function escapeRegExp(str) {
|
|
793
997
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
794
998
|
}
|
|
@@ -1035,7 +1239,9 @@ function attachDOMBinding(clientProto) {
|
|
|
1035
1239
|
const scheduleFn = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => setTimeout(cb, 0);
|
|
1036
1240
|
scheduleFn(() => {
|
|
1037
1241
|
pendingUpdates.forEach((html, el) => {
|
|
1038
|
-
|
|
1242
|
+
if (el.isConnected !== false) {
|
|
1243
|
+
patchDOM(el, html);
|
|
1244
|
+
}
|
|
1039
1245
|
});
|
|
1040
1246
|
pendingUpdates.clear();
|
|
1041
1247
|
rafScheduled = false;
|
|
@@ -1174,7 +1380,7 @@ function attachDOMBinding(clientProto) {
|
|
|
1174
1380
|
if (this._domInitialized) return;
|
|
1175
1381
|
this._domInitialized = true;
|
|
1176
1382
|
const PUSH_EVENTS = ["input", "change", "keyup", "paste", "blur"];
|
|
1177
|
-
const debounceTimers = /* @__PURE__ */ new
|
|
1383
|
+
const debounceTimers = /* @__PURE__ */ new WeakMap();
|
|
1178
1384
|
PUSH_EVENTS.forEach((evtName) => {
|
|
1179
1385
|
this.addDomListener(document, evtName, (e) => {
|
|
1180
1386
|
if (!e.target || !e.target.getAttribute) return;
|
|
@@ -1227,6 +1433,14 @@ function attachDOMBinding(clientProto) {
|
|
|
1227
1433
|
const rtTopic = e.target.getAttribute("data-rt-submit");
|
|
1228
1434
|
const apiTarget = e.target.getAttribute("data-api-submit");
|
|
1229
1435
|
if (rtTopic || apiTarget) {
|
|
1436
|
+
const formInputs = e.target.querySelectorAll("[name]");
|
|
1437
|
+
formInputs.forEach((inputEl) => {
|
|
1438
|
+
const name = inputEl.name;
|
|
1439
|
+
if (name) {
|
|
1440
|
+
this.publish(`errors/${name}`, "");
|
|
1441
|
+
inputEl.classList.remove("invalid");
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1230
1444
|
const validatedInputs = e.target.querySelectorAll("[data-rt-validate]");
|
|
1231
1445
|
let formIsValid = true;
|
|
1232
1446
|
if (validatedInputs.length > 0 && typeof this.validateField === "function") {
|
|
@@ -1240,9 +1454,6 @@ function attachDOMBinding(clientProto) {
|
|
|
1240
1454
|
formIsValid = false;
|
|
1241
1455
|
inputEl.classList.add("invalid");
|
|
1242
1456
|
this.publish(`errors/${name}`, errorMsg);
|
|
1243
|
-
} else {
|
|
1244
|
-
inputEl.classList.remove("invalid");
|
|
1245
|
-
this.publish(`errors/${name}`, "");
|
|
1246
1457
|
}
|
|
1247
1458
|
}
|
|
1248
1459
|
});
|
|
@@ -1270,8 +1481,11 @@ function attachDOMBinding(clientProto) {
|
|
|
1270
1481
|
resolvedTarget = resolvedTarget.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
|
|
1271
1482
|
}
|
|
1272
1483
|
const parts = resolvedTarget.trim().split(" ");
|
|
1273
|
-
|
|
1484
|
+
let method = parts.length > 1 ? parts[0].toUpperCase() : "POST";
|
|
1274
1485
|
const path = parts.length > 1 ? parts[1] : parts[0];
|
|
1486
|
+
if (data._method) {
|
|
1487
|
+
method = String(data._method).toUpperCase();
|
|
1488
|
+
}
|
|
1275
1489
|
try {
|
|
1276
1490
|
const result = await this.api.request(method, path, data);
|
|
1277
1491
|
const resultBind = e.target.getAttribute("data-api-result");
|
|
@@ -1358,6 +1572,8 @@ function attachDOMBinding(clientProto) {
|
|
|
1358
1572
|
});
|
|
1359
1573
|
this._scanAndFetchAPIBinds();
|
|
1360
1574
|
this._scanStoreBinds();
|
|
1575
|
+
this._resolveImports();
|
|
1576
|
+
this._initSPARouter();
|
|
1361
1577
|
};
|
|
1362
1578
|
clientProto._scanAndFetchAPIBinds = async function() {
|
|
1363
1579
|
if (typeof document === "undefined") return;
|
|
@@ -1365,6 +1581,10 @@ function attachDOMBinding(clientProto) {
|
|
|
1365
1581
|
for (const el of Array.from(elements)) {
|
|
1366
1582
|
const path = el.getAttribute("data-api-get");
|
|
1367
1583
|
if (!path) continue;
|
|
1584
|
+
if (typeof el.hasAttribute === "function" && el.hasAttribute("data-api-initialized")) continue;
|
|
1585
|
+
if (typeof el.setAttribute === "function") {
|
|
1586
|
+
el.setAttribute("data-api-initialized", "true");
|
|
1587
|
+
}
|
|
1368
1588
|
try {
|
|
1369
1589
|
const result = await this.api.get(path);
|
|
1370
1590
|
const apiStore = el.getAttribute("data-api-store");
|
|
@@ -1394,7 +1614,8 @@ function attachDOMBinding(clientProto) {
|
|
|
1394
1614
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1395
1615
|
el.value = typeof result === "object" ? result.value !== void 0 ? result.value : "" : result;
|
|
1396
1616
|
} else {
|
|
1397
|
-
|
|
1617
|
+
const rawHTML = typeof result === "object" ? result.html || result.text || JSON.stringify(result) : String(result);
|
|
1618
|
+
el.innerHTML = sanitizeHTML(rawHTML);
|
|
1398
1619
|
}
|
|
1399
1620
|
}
|
|
1400
1621
|
}
|
|
@@ -1611,11 +1832,158 @@ function attachDOMBinding(clientProto) {
|
|
|
1611
1832
|
}
|
|
1612
1833
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1613
1834
|
el.value = typeof processedPayload === "object" ? processedPayload.value !== void 0 ? processedPayload.value : "" : processedPayload;
|
|
1835
|
+
} else if (template) {
|
|
1836
|
+
el.innerHTML = typeof processedPayload === "object" ? processedPayload.html || processedPayload.text || JSON.stringify(processedPayload) : String(processedPayload);
|
|
1614
1837
|
} else {
|
|
1615
|
-
|
|
1838
|
+
const rawHTML = typeof processedPayload === "object" ? processedPayload.html || processedPayload.text || JSON.stringify(processedPayload) : String(processedPayload);
|
|
1839
|
+
el.innerHTML = sanitizeHTML(rawHTML);
|
|
1616
1840
|
}
|
|
1617
1841
|
});
|
|
1618
1842
|
};
|
|
1843
|
+
clientProto._resolveImports = async function(container) {
|
|
1844
|
+
if (typeof document === "undefined") return;
|
|
1845
|
+
const root = container || document.body || document;
|
|
1846
|
+
if (!root || typeof root.querySelectorAll !== "function") return;
|
|
1847
|
+
const elements = root.querySelectorAll("[data-import]");
|
|
1848
|
+
if (elements.length === 0) return;
|
|
1849
|
+
const resolveNode = async (el, resolvingSet) => {
|
|
1850
|
+
const src = el.getAttribute("data-import");
|
|
1851
|
+
if (!src) return;
|
|
1852
|
+
if (resolvingSet.has(src)) {
|
|
1853
|
+
console.warn(`[Dolphin Component Warning]: Circular import detected for "${src}". Skipping resolving.`);
|
|
1854
|
+
el.innerHTML = `<span style="color:red;font-weight:bold;">Circular import: ${src}</span>`;
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1857
|
+
resolvingSet.add(src);
|
|
1858
|
+
let promise = componentPromiseCache.get(src);
|
|
1859
|
+
if (!promise) {
|
|
1860
|
+
promise = fetch(src).then((res) => {
|
|
1861
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1862
|
+
return res.text();
|
|
1863
|
+
});
|
|
1864
|
+
promise.catch(() => componentPromiseCache.delete(src));
|
|
1865
|
+
componentPromiseCache.set(src, promise);
|
|
1866
|
+
}
|
|
1867
|
+
let content = "";
|
|
1868
|
+
try {
|
|
1869
|
+
content = await promise;
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
console.error(`[Dolphin Component Error]: Failed to fetch component "${src}":`, err);
|
|
1872
|
+
content = `<span style="color:red;font-weight:bold;">Failed to import ${src}</span>`;
|
|
1873
|
+
}
|
|
1874
|
+
el.innerHTML = sanitizeHTML(content);
|
|
1875
|
+
el.removeAttribute("data-import");
|
|
1876
|
+
const nestedElements = el.querySelectorAll("[data-import]");
|
|
1877
|
+
if (nestedElements.length > 0) {
|
|
1878
|
+
const subPromises = Array.from(nestedElements).map((child) => resolveNode(child, new Set(resolvingSet)));
|
|
1879
|
+
await Promise.all(subPromises);
|
|
1880
|
+
}
|
|
1881
|
+
this._scanStoreBinds();
|
|
1882
|
+
this._scanAndFetchAPIBinds();
|
|
1883
|
+
};
|
|
1884
|
+
const promises = Array.from(elements).map((el) => resolveNode(el, /* @__PURE__ */ new Set()));
|
|
1885
|
+
await Promise.all(promises);
|
|
1886
|
+
};
|
|
1887
|
+
clientProto._initSPARouter = function() {
|
|
1888
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1889
|
+
if (this._routerInitialized) return;
|
|
1890
|
+
this._routerInitialized = true;
|
|
1891
|
+
let _spaAbortController = null;
|
|
1892
|
+
const findViewport = () => {
|
|
1893
|
+
const selector = this.options.routerViewport || "main, #viewport, body";
|
|
1894
|
+
const selectors = selector.split(",").map((s) => s.trim());
|
|
1895
|
+
for (const sel of selectors) {
|
|
1896
|
+
const el = document.querySelector(sel);
|
|
1897
|
+
if (el) return el;
|
|
1898
|
+
}
|
|
1899
|
+
return document.body;
|
|
1900
|
+
};
|
|
1901
|
+
const loadPage = async (url, pushState = true) => {
|
|
1902
|
+
try {
|
|
1903
|
+
if (this.options.debug) {
|
|
1904
|
+
console.log(`%c\u{1F6E3}\uFE0F [Dolphin Router]: Navigating to ${url}...`, "color: #3b82f6; font-weight: bold;");
|
|
1905
|
+
}
|
|
1906
|
+
if (_spaAbortController) {
|
|
1907
|
+
_spaAbortController.abort();
|
|
1908
|
+
}
|
|
1909
|
+
_spaAbortController = new AbortController();
|
|
1910
|
+
const signal = _spaAbortController.signal;
|
|
1911
|
+
const viewport = findViewport();
|
|
1912
|
+
if (this.options.routerTransitions && viewport) {
|
|
1913
|
+
viewport.classList.add("dolphin-fade-out");
|
|
1914
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1915
|
+
}
|
|
1916
|
+
const response = await fetch(url, { signal });
|
|
1917
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1918
|
+
const html = await response.text();
|
|
1919
|
+
_spaAbortController = null;
|
|
1920
|
+
const parser = new DOMParser();
|
|
1921
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
1922
|
+
if (doc.title) {
|
|
1923
|
+
document.title = doc.title;
|
|
1924
|
+
}
|
|
1925
|
+
const newViewport = doc.querySelector(this.options.routerViewport || "main, #viewport, body");
|
|
1926
|
+
const currentViewport = findViewport();
|
|
1927
|
+
if (newViewport && currentViewport) {
|
|
1928
|
+
currentViewport.innerHTML = newViewport.innerHTML;
|
|
1929
|
+
Array.from(newViewport.attributes).forEach((attr) => {
|
|
1930
|
+
currentViewport.setAttribute(attr.name, attr.value);
|
|
1931
|
+
});
|
|
1932
|
+
} else if (currentViewport) {
|
|
1933
|
+
currentViewport.innerHTML = doc.body.innerHTML;
|
|
1934
|
+
}
|
|
1935
|
+
if (pushState) {
|
|
1936
|
+
window.history.pushState({ dolphinSpa: true, url }, "", url);
|
|
1937
|
+
}
|
|
1938
|
+
if (this.options.routerTransitions && currentViewport) {
|
|
1939
|
+
currentViewport.classList.remove("dolphin-fade-out");
|
|
1940
|
+
currentViewport.classList.add("dolphin-fade-in");
|
|
1941
|
+
setTimeout(() => currentViewport.classList.remove("dolphin-fade-in"), 300);
|
|
1942
|
+
}
|
|
1943
|
+
await this._resolveImports(currentViewport);
|
|
1944
|
+
this._scanStoreBinds();
|
|
1945
|
+
this._scanAndFetchAPIBinds();
|
|
1946
|
+
} catch (err) {
|
|
1947
|
+
if (err && err.name === "AbortError") return;
|
|
1948
|
+
console.error("[Dolphin Router Error]: Failed to route page:", err);
|
|
1949
|
+
window.location.href = url;
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
this.addDomListener(document, "click", (e) => {
|
|
1953
|
+
const anchor = e.target.closest("a");
|
|
1954
|
+
if (!anchor) return;
|
|
1955
|
+
if (!anchor.hasAttribute("data-spa") && anchor.getAttribute("data-spa") !== "true") return;
|
|
1956
|
+
const href = anchor.getAttribute("href");
|
|
1957
|
+
if (!href || href.startsWith("#") || href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:")) return;
|
|
1958
|
+
const url = new URL(href, window.location.href);
|
|
1959
|
+
if (url.origin !== window.location.origin) return;
|
|
1960
|
+
e.preventDefault();
|
|
1961
|
+
loadPage(href);
|
|
1962
|
+
});
|
|
1963
|
+
this.addDomListener(window, "popstate", (e) => {
|
|
1964
|
+
if (e.state && e.state.dolphinSpa) {
|
|
1965
|
+
loadPage(e.state.url, false);
|
|
1966
|
+
} else if (e.state === null) {
|
|
1967
|
+
loadPage(window.location.pathname, false);
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
if (this.options.routerTransitions) {
|
|
1971
|
+
const style = document.createElement("style");
|
|
1972
|
+
style.innerHTML = `
|
|
1973
|
+
.dolphin-fade-out {
|
|
1974
|
+
opacity: 0;
|
|
1975
|
+
transition: opacity 0.15s ease-in-out;
|
|
1976
|
+
}
|
|
1977
|
+
.dolphin-fade-in {
|
|
1978
|
+
opacity: 0;
|
|
1979
|
+
}
|
|
1980
|
+
main, #viewport, body {
|
|
1981
|
+
transition: opacity 0.15s ease-in-out;
|
|
1982
|
+
}
|
|
1983
|
+
`;
|
|
1984
|
+
document.head.appendChild(style);
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1619
1987
|
}
|
|
1620
1988
|
|
|
1621
1989
|
// src/offline.ts
|
|
@@ -1693,6 +2061,10 @@ var DolphinOffline = class {
|
|
|
1693
2061
|
const store = transaction.objectStore("cache");
|
|
1694
2062
|
store.put({ data, timestamp: Date.now() }, key);
|
|
1695
2063
|
transaction.oncomplete = () => resolve();
|
|
2064
|
+
transaction.onerror = () => {
|
|
2065
|
+
console.warn("[Dolphin Offline] setCache write failed for key:", key);
|
|
2066
|
+
resolve();
|
|
2067
|
+
};
|
|
1696
2068
|
} catch {
|
|
1697
2069
|
resolve();
|
|
1698
2070
|
}
|
|
@@ -1715,6 +2087,11 @@ var DolphinOffline = class {
|
|
|
1715
2087
|
const store = transaction.objectStore("mutations");
|
|
1716
2088
|
store.add(mutation);
|
|
1717
2089
|
transaction.oncomplete = () => resolve();
|
|
2090
|
+
transaction.onerror = () => {
|
|
2091
|
+
console.warn("[Dolphin Offline] queueMutation write failed:", method, path);
|
|
2092
|
+
this.memoryMutations.push(mutation);
|
|
2093
|
+
resolve();
|
|
2094
|
+
};
|
|
1718
2095
|
} catch {
|
|
1719
2096
|
resolve();
|
|
1720
2097
|
}
|
|
@@ -1747,6 +2124,10 @@ var DolphinOffline = class {
|
|
|
1747
2124
|
const store = transaction.objectStore("mutations");
|
|
1748
2125
|
store.delete(id);
|
|
1749
2126
|
transaction.oncomplete = () => resolve();
|
|
2127
|
+
transaction.onerror = () => {
|
|
2128
|
+
console.warn("[Dolphin Offline] removeMutation failed for id:", id);
|
|
2129
|
+
resolve();
|
|
2130
|
+
};
|
|
1750
2131
|
} catch {
|
|
1751
2132
|
resolve();
|
|
1752
2133
|
}
|
|
@@ -2092,6 +2473,9 @@ function attachDragDrop(clientProto) {
|
|
|
2092
2473
|
function attachCollab(clientProto) {
|
|
2093
2474
|
clientProto._initCollab = function() {
|
|
2094
2475
|
if (typeof document === "undefined") return;
|
|
2476
|
+
const remoteCursors = /* @__PURE__ */ new Map();
|
|
2477
|
+
const cursorStaleTimers = /* @__PURE__ */ new Map();
|
|
2478
|
+
const CURSOR_STALE_MS = 5e3;
|
|
2095
2479
|
this.addDomListener(document, "mousemove", (e) => {
|
|
2096
2480
|
const shareContainers = document.querySelectorAll("[data-rt-cursor-share]");
|
|
2097
2481
|
shareContainers.forEach((container) => {
|
|
@@ -2128,6 +2512,7 @@ function attachCollab(clientProto) {
|
|
|
2128
2512
|
if (e.target._typingTimer) clearTimeout(e.target._typingTimer);
|
|
2129
2513
|
e.target._typingTimer = setTimeout(() => {
|
|
2130
2514
|
e.target._isTyping = false;
|
|
2515
|
+
e.target._typingTimer = null;
|
|
2131
2516
|
publishTyping(false);
|
|
2132
2517
|
}, 2e3);
|
|
2133
2518
|
});
|
|
@@ -2151,21 +2536,32 @@ function attachCollab(clientProto) {
|
|
|
2151
2536
|
if (remoteDeviceId === this.deviceId) return;
|
|
2152
2537
|
const container = document.querySelector(`[data-rt-cursor-share="${room}"]`);
|
|
2153
2538
|
if (!container) return;
|
|
2154
|
-
|
|
2155
|
-
|
|
2539
|
+
const cursorKey = `${room}::${remoteDeviceId}`;
|
|
2540
|
+
let cursorEl = remoteCursors.get(cursorKey);
|
|
2541
|
+
if (!cursorEl || !document.contains(cursorEl)) {
|
|
2156
2542
|
cursorEl = document.createElement("div");
|
|
2157
2543
|
cursorEl.className = `rt-cursor rt-cursor-${remoteDeviceId}`;
|
|
2158
2544
|
cursorEl.style.position = "absolute";
|
|
2159
2545
|
cursorEl.style.width = "10px";
|
|
2160
2546
|
cursorEl.style.height = "10px";
|
|
2161
2547
|
cursorEl.style.borderRadius = "50%";
|
|
2162
|
-
cursorEl.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16);
|
|
2548
|
+
cursorEl.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0");
|
|
2163
2549
|
cursorEl.style.pointerEvents = "none";
|
|
2164
2550
|
container.appendChild(cursorEl);
|
|
2551
|
+
remoteCursors.set(cursorKey, cursorEl);
|
|
2165
2552
|
}
|
|
2166
2553
|
const box = container.getBoundingClientRect();
|
|
2167
2554
|
cursorEl.style.left = payload.x * box.width + "px";
|
|
2168
2555
|
cursorEl.style.top = payload.y * box.height + "px";
|
|
2556
|
+
if (cursorStaleTimers.has(cursorKey)) {
|
|
2557
|
+
clearTimeout(cursorStaleTimers.get(cursorKey));
|
|
2558
|
+
}
|
|
2559
|
+
cursorStaleTimers.set(cursorKey, setTimeout(() => {
|
|
2560
|
+
const el = remoteCursors.get(cursorKey);
|
|
2561
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
2562
|
+
remoteCursors.delete(cursorKey);
|
|
2563
|
+
cursorStaleTimers.delete(cursorKey);
|
|
2564
|
+
}, CURSOR_STALE_MS));
|
|
2169
2565
|
});
|
|
2170
2566
|
this.subscribe("collab/+/crdt", (payload, topic) => {
|
|
2171
2567
|
if (payload.deviceId === this.deviceId) return;
|
|
@@ -2183,6 +2579,14 @@ function attachCollab(clientProto) {
|
|
|
2183
2579
|
}
|
|
2184
2580
|
});
|
|
2185
2581
|
});
|
|
2582
|
+
this._collabCleanup = () => {
|
|
2583
|
+
cursorStaleTimers.forEach((t) => clearTimeout(t));
|
|
2584
|
+
cursorStaleTimers.clear();
|
|
2585
|
+
remoteCursors.forEach((el) => {
|
|
2586
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
2587
|
+
});
|
|
2588
|
+
remoteCursors.clear();
|
|
2589
|
+
};
|
|
2186
2590
|
};
|
|
2187
2591
|
}
|
|
2188
2592
|
|