dolphin-client 1.0.5 → 1.0.6
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 +25 -2
- 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 +1 -1
package/dist/index.js
CHANGED
|
@@ -22,7 +22,21 @@ var APIHandler = class {
|
|
|
22
22
|
target.requestDirect = (method, path, body, options) => {
|
|
23
23
|
return this.requestDirect(method, path, body, options);
|
|
24
24
|
};
|
|
25
|
-
|
|
25
|
+
target._findCSRFToken = () => this._findCSRFToken();
|
|
26
|
+
target._resolveBaseUrl = (path) => this._resolveBaseUrl(path);
|
|
27
|
+
target._normalizeValidationErrors = (errData) => this._normalizeValidationErrors(errData);
|
|
28
|
+
const methods = [
|
|
29
|
+
"get",
|
|
30
|
+
"post",
|
|
31
|
+
"put",
|
|
32
|
+
"patch",
|
|
33
|
+
"del",
|
|
34
|
+
"request",
|
|
35
|
+
"requestDirect",
|
|
36
|
+
"_findCSRFToken",
|
|
37
|
+
"_resolveBaseUrl",
|
|
38
|
+
"_normalizeValidationErrors"
|
|
39
|
+
];
|
|
26
40
|
return new Proxy(target, {
|
|
27
41
|
get: (t, prop) => {
|
|
28
42
|
if (typeof prop === "string" && !methods.includes(prop)) {
|
|
@@ -32,6 +46,108 @@ var APIHandler = class {
|
|
|
32
46
|
}
|
|
33
47
|
});
|
|
34
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Attempts to find a CSRF token in the document (meta tags, forms, or cookies).
|
|
51
|
+
* Works for Laravel, CakePHP, WordPress, Express, NestJS, etc.
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
_findCSRFToken() {
|
|
55
|
+
if (typeof document === "undefined") return null;
|
|
56
|
+
const metaNames = ["csrf-token", "_csrf", "xsrf-token", "csrf_token"];
|
|
57
|
+
for (const name of metaNames) {
|
|
58
|
+
const metaEl = document.querySelector(`meta[name="${name}"], meta[content][name$="${name}"]`);
|
|
59
|
+
if (metaEl) {
|
|
60
|
+
const token = metaEl.getAttribute("content");
|
|
61
|
+
if (token) return token;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const inputNames = ["_csrfToken", "_token", "_csrf", "csrf_token"];
|
|
65
|
+
for (const name of inputNames) {
|
|
66
|
+
const inputEl = document.querySelector(`input[type="hidden"][name="${name}"]`);
|
|
67
|
+
if (inputEl && inputEl.value) return inputEl.value;
|
|
68
|
+
}
|
|
69
|
+
const cookies = ["csrfToken", "XSRF-TOKEN", "_csrf", "csrf_token"];
|
|
70
|
+
for (const name of cookies) {
|
|
71
|
+
const match = document.cookie.match(new RegExp("(^|;\\s*)" + name + "=([^;]*)"));
|
|
72
|
+
if (match) return decodeURIComponent(match[2]);
|
|
73
|
+
}
|
|
74
|
+
const wpNonce = typeof window !== "undefined" && window.wpApiSettings?.nonce;
|
|
75
|
+
if (wpNonce) return wpNonce;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Dynamically resolves the Base Path/URL from `<base href="...">` or subfolders.
|
|
80
|
+
* @private
|
|
81
|
+
*/
|
|
82
|
+
_resolveBaseUrl(path) {
|
|
83
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
84
|
+
return path;
|
|
85
|
+
}
|
|
86
|
+
let baseUrl = this.client.httpUrl;
|
|
87
|
+
if (typeof document !== "undefined") {
|
|
88
|
+
const baseEl = document.querySelector("base[href]");
|
|
89
|
+
if (baseEl) {
|
|
90
|
+
const href = baseEl.getAttribute("href") || "";
|
|
91
|
+
if (href && href !== "/") {
|
|
92
|
+
const cleanHref = href.endsWith("/") ? href.slice(0, -1) : href;
|
|
93
|
+
baseUrl = `${this.client.httpUrl}${cleanHref.startsWith("/") ? cleanHref : "/" + cleanHref}`;
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
const metaBase = document.querySelector('meta[name="base-path"]');
|
|
97
|
+
if (metaBase) {
|
|
98
|
+
const content = metaBase.getAttribute("content") || "";
|
|
99
|
+
if (content && content !== "/") {
|
|
100
|
+
const cleanContent = content.endsWith("/") ? content.slice(0, -1) : content;
|
|
101
|
+
baseUrl = `${this.client.httpUrl}${cleanContent.startsWith("/") ? cleanContent : "/" + cleanContent}`;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const cleanPath = path.startsWith("/") ? path : "/" + path;
|
|
107
|
+
return `${baseUrl}${cleanPath}`;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Normalizes backend validation errors from major PHP and Node.js frameworks
|
|
111
|
+
* into a unified { [field]: message } object.
|
|
112
|
+
* @private
|
|
113
|
+
*/
|
|
114
|
+
_normalizeValidationErrors(errData) {
|
|
115
|
+
const normalized = {};
|
|
116
|
+
if (!errData || typeof errData !== "object") return normalized;
|
|
117
|
+
const errors = errData.errors || errData.validationErrors || errData;
|
|
118
|
+
if (Array.isArray(errors)) {
|
|
119
|
+
for (const err of errors) {
|
|
120
|
+
if (err && typeof err === "object") {
|
|
121
|
+
const field = err.path || err.param || err.field || err.property;
|
|
122
|
+
const msg = err.msg || err.message || err.error;
|
|
123
|
+
if (field && msg) {
|
|
124
|
+
normalized[field] = Array.isArray(msg) ? msg[0] : msg;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return normalized;
|
|
129
|
+
}
|
|
130
|
+
if (typeof errors === "object" && errors !== null) {
|
|
131
|
+
for (const key in errors) {
|
|
132
|
+
const val = errors[key];
|
|
133
|
+
if (!val) continue;
|
|
134
|
+
if (Array.isArray(val)) {
|
|
135
|
+
if (val.length > 0) {
|
|
136
|
+
normalized[key] = String(val[0]);
|
|
137
|
+
}
|
|
138
|
+
} else if (typeof val === "object") {
|
|
139
|
+
const innerKeys = Object.keys(val);
|
|
140
|
+
if (innerKeys.length > 0) {
|
|
141
|
+
const firstInnerKey = innerKeys[0];
|
|
142
|
+
normalized[key] = String(val[firstInnerKey]);
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
normalized[key] = String(val);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return normalized;
|
|
150
|
+
}
|
|
35
151
|
/**
|
|
36
152
|
* Intercept request for offline-first caching and queuing.
|
|
37
153
|
*/
|
|
@@ -82,30 +198,68 @@ var APIHandler = class {
|
|
|
82
198
|
*/
|
|
83
199
|
async requestDirect(method, path, body = null, options = {}) {
|
|
84
200
|
const _isRetry = options._isRetry === true;
|
|
85
|
-
|
|
201
|
+
let finalMethod = method.toUpperCase();
|
|
202
|
+
let finalBody = body;
|
|
203
|
+
const headers = {
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
...options.headers || {}
|
|
206
|
+
};
|
|
207
|
+
if (["PUT", "PATCH", "DELETE"].includes(finalMethod)) {
|
|
208
|
+
if (this.client.options.methodSpoofing || options.methodSpoofing) {
|
|
209
|
+
headers["X-HTTP-Method-Override"] = finalMethod;
|
|
210
|
+
if (finalBody instanceof FormData) {
|
|
211
|
+
finalBody.append("_method", finalMethod);
|
|
212
|
+
} else if (finalBody && typeof finalBody === "object") {
|
|
213
|
+
finalBody = {
|
|
214
|
+
...finalBody,
|
|
215
|
+
_method: finalMethod
|
|
216
|
+
};
|
|
217
|
+
} else if (!finalBody) {
|
|
218
|
+
finalBody = { _method: finalMethod };
|
|
219
|
+
}
|
|
220
|
+
finalMethod = "POST";
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const url = this._resolveBaseUrl(path);
|
|
86
224
|
if (this.client.options.debug) {
|
|
87
|
-
console.log(`%c\u{1F680} [Dolphin API Request]:`, "color: #3b82f6; font-weight: bold;", method.toUpperCase(), path,
|
|
225
|
+
console.log(`%c\u{1F680} [Dolphin API Request]:`, "color: #3b82f6; font-weight: bold;", method.toUpperCase(), path, finalBody || "");
|
|
88
226
|
}
|
|
89
227
|
const controller = new AbortController();
|
|
90
228
|
const timeoutId = setTimeout(
|
|
91
229
|
() => controller.abort(),
|
|
92
230
|
this.client.options.timeout
|
|
93
231
|
);
|
|
94
|
-
const headers = {
|
|
95
|
-
"Content-Type": "application/json",
|
|
96
|
-
...options.headers || {}
|
|
97
|
-
};
|
|
98
232
|
if (this.client.accessToken) {
|
|
99
233
|
headers["Authorization"] = `Bearer ${this.client.accessToken}`;
|
|
100
234
|
}
|
|
235
|
+
if (["POST", "PUT", "PATCH", "DELETE"].includes(method.toUpperCase())) {
|
|
236
|
+
const csrfToken = this._findCSRFToken();
|
|
237
|
+
if (csrfToken) {
|
|
238
|
+
headers["X-CSRF-Token"] = csrfToken;
|
|
239
|
+
headers["X-XSRF-TOKEN"] = csrfToken;
|
|
240
|
+
headers["X-CSRFToken"] = csrfToken;
|
|
241
|
+
headers["X-WP-Nonce"] = csrfToken;
|
|
242
|
+
if (finalBody && typeof finalBody === "object") {
|
|
243
|
+
if (!finalBody._csrfToken && !finalBody._token && !finalBody._csrf) {
|
|
244
|
+
finalBody = {
|
|
245
|
+
...finalBody,
|
|
246
|
+
_csrfToken: csrfToken,
|
|
247
|
+
_token: csrfToken,
|
|
248
|
+
_csrf: csrfToken
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
101
254
|
const fetchOptions = { ...options };
|
|
102
255
|
delete fetchOptions._isRetry;
|
|
256
|
+
delete fetchOptions.methodSpoofing;
|
|
103
257
|
try {
|
|
104
258
|
const response = await fetch(url, {
|
|
105
|
-
method,
|
|
259
|
+
method: finalMethod,
|
|
106
260
|
headers,
|
|
107
261
|
signal: controller.signal,
|
|
108
|
-
...
|
|
262
|
+
...finalBody ? { body: JSON.stringify(finalBody) } : {},
|
|
109
263
|
...fetchOptions
|
|
110
264
|
});
|
|
111
265
|
clearTimeout(timeoutId);
|
|
@@ -137,6 +291,14 @@ var APIHandler = class {
|
|
|
137
291
|
if (this.client.options.debug) {
|
|
138
292
|
console.error(`%c\u274C [Dolphin API Error]:`, "color: #ef4444; font-weight: bold;", method.toUpperCase(), path, err);
|
|
139
293
|
}
|
|
294
|
+
if (err && typeof err === "object" && err.data) {
|
|
295
|
+
const normErrors = this._normalizeValidationErrors(err.data);
|
|
296
|
+
if (Object.keys(normErrors).length > 0) {
|
|
297
|
+
for (const field in normErrors) {
|
|
298
|
+
this.client.publish(`errors/${field}`, normErrors[field]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
140
302
|
if (err.name === "AbortError") {
|
|
141
303
|
throw { status: 408, data: { error: "Request timed out" } };
|
|
142
304
|
}
|
|
@@ -270,12 +432,15 @@ var DolphinStore = class {
|
|
|
270
432
|
data;
|
|
271
433
|
listeners;
|
|
272
434
|
subscribed;
|
|
435
|
+
/** @fix: Store unsubscribe functions so destroy() can clean up WS subscriptions (was: subscriptions never removed) */
|
|
436
|
+
_unsubscribers;
|
|
273
437
|
/** @param {DolphinClient} client */
|
|
274
438
|
constructor(client) {
|
|
275
439
|
this.client = client;
|
|
276
440
|
this.data = /* @__PURE__ */ new Map();
|
|
277
441
|
this.listeners = /* @__PURE__ */ new Set();
|
|
278
442
|
this.subscribed = /* @__PURE__ */ new Set();
|
|
443
|
+
this._unsubscribers = /* @__PURE__ */ new Map();
|
|
279
444
|
return new Proxy(this, {
|
|
280
445
|
get: (target, prop) => {
|
|
281
446
|
if (prop in target) return target[prop];
|
|
@@ -327,9 +492,12 @@ var DolphinStore = class {
|
|
|
327
492
|
state.error = null;
|
|
328
493
|
this._applyTransform(state);
|
|
329
494
|
if (!this.subscribed.has(name)) {
|
|
330
|
-
this.client.
|
|
495
|
+
const unsubscribe = () => this.client.unsubscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
|
|
496
|
+
const updateHandler = (update) => {
|
|
331
497
|
this._handleRemoteUpdate(name, update);
|
|
332
|
-
}
|
|
498
|
+
};
|
|
499
|
+
this.client.subscribe(`db:sync/${name.toLowerCase()}`, updateHandler);
|
|
500
|
+
this._unsubscribers.set(name, unsubscribe);
|
|
333
501
|
this.subscribed.add(name);
|
|
334
502
|
}
|
|
335
503
|
} catch (e) {
|
|
@@ -386,6 +554,22 @@ var DolphinStore = class {
|
|
|
386
554
|
_notify() {
|
|
387
555
|
this.listeners.forEach((l) => l());
|
|
388
556
|
}
|
|
557
|
+
/**
|
|
558
|
+
* Clean up all WebSocket subscriptions and listeners.
|
|
559
|
+
* Call this when the store is no longer needed to prevent resource leaks.
|
|
560
|
+
*/
|
|
561
|
+
destroy() {
|
|
562
|
+
this._unsubscribers.forEach((unsub) => {
|
|
563
|
+
try {
|
|
564
|
+
unsub();
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
this._unsubscribers.clear();
|
|
569
|
+
this.subscribed.clear();
|
|
570
|
+
this.listeners.clear();
|
|
571
|
+
this.data.clear();
|
|
572
|
+
}
|
|
389
573
|
};
|
|
390
574
|
|
|
391
575
|
// src/core.ts
|
|
@@ -405,6 +589,8 @@ var DolphinClient = class {
|
|
|
405
589
|
fileHandlers;
|
|
406
590
|
_offlineQueue;
|
|
407
591
|
reconnectAttempts;
|
|
592
|
+
/** @fix: Store timer ID so disconnect() can cancel pending reconnects (was: memory/logic leak) */
|
|
593
|
+
_reconnectTimer;
|
|
408
594
|
_attachedListeners;
|
|
409
595
|
constructor(url = "", deviceId = "", options = {}) {
|
|
410
596
|
if (!url && typeof window !== "undefined") url = window.location.host;
|
|
@@ -414,7 +600,7 @@ var DolphinClient = class {
|
|
|
414
600
|
else if (typeof window !== "undefined") protocol = window.location.protocol;
|
|
415
601
|
this.host = (url || "localhost").replace(/\/$/, "").replace(/^https?:\/\//, "");
|
|
416
602
|
this.httpUrl = `${protocol}//${this.host}`;
|
|
417
|
-
this.deviceId = deviceId || "web_" + Math.random().toString(36).
|
|
603
|
+
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));
|
|
418
604
|
this.options = {
|
|
419
605
|
timeout: 15e3,
|
|
420
606
|
chunkSize: 65536,
|
|
@@ -422,6 +608,9 @@ var DolphinClient = class {
|
|
|
422
608
|
maxReconnect: 5,
|
|
423
609
|
autoRefreshToken: true,
|
|
424
610
|
debug: false,
|
|
611
|
+
methodSpoofing: false,
|
|
612
|
+
routerViewport: "main, #viewport, body",
|
|
613
|
+
routerTransitions: true,
|
|
425
614
|
...options
|
|
426
615
|
};
|
|
427
616
|
this.socket = null;
|
|
@@ -441,6 +630,7 @@ var DolphinClient = class {
|
|
|
441
630
|
this.fileHandlers = /* @__PURE__ */ new Set();
|
|
442
631
|
this._offlineQueue = [];
|
|
443
632
|
this.reconnectAttempts = 0;
|
|
633
|
+
this._reconnectTimer = null;
|
|
444
634
|
this._attachedListeners = [];
|
|
445
635
|
if (typeof window !== "undefined" && typeof this._initDOMBinding === "function") {
|
|
446
636
|
this._initDOMBinding();
|
|
@@ -469,6 +659,9 @@ var DolphinClient = class {
|
|
|
469
659
|
// ── WebSocket ─────────────────────────────────────────────────────────────
|
|
470
660
|
/** Connect to the Dolphin realtime server */
|
|
471
661
|
async connect() {
|
|
662
|
+
if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) {
|
|
663
|
+
return Promise.resolve();
|
|
664
|
+
}
|
|
472
665
|
return new Promise((resolve, reject) => {
|
|
473
666
|
const protocol = this.httpUrl.startsWith("https") ? "wss:" : "ws:";
|
|
474
667
|
const wsUrl = `${protocol}//${this.host}/realtime?deviceId=${this.deviceId}`;
|
|
@@ -493,11 +686,18 @@ var DolphinClient = class {
|
|
|
493
686
|
}
|
|
494
687
|
/** Disconnect cleanly */
|
|
495
688
|
disconnect() {
|
|
689
|
+
if (this._reconnectTimer !== null) {
|
|
690
|
+
clearTimeout(this._reconnectTimer);
|
|
691
|
+
this._reconnectTimer = null;
|
|
692
|
+
}
|
|
496
693
|
if (this.socket) {
|
|
497
694
|
this.socket.onclose = null;
|
|
498
695
|
this.socket.close();
|
|
499
696
|
this.socket = null;
|
|
500
697
|
}
|
|
698
|
+
if (typeof this._collabCleanup === "function") {
|
|
699
|
+
this._collabCleanup();
|
|
700
|
+
}
|
|
501
701
|
this.cleanupDomListeners();
|
|
502
702
|
}
|
|
503
703
|
/** @private */
|
|
@@ -585,8 +785,11 @@ var DolphinClient = class {
|
|
|
585
785
|
this.reconnectAttempts++;
|
|
586
786
|
const delay = Math.pow(2, this.reconnectAttempts) * 1e3;
|
|
587
787
|
console.log(`[Dolphin] Reconnecting in ${delay / 1e3}s (attempt ${this.reconnectAttempts})...`);
|
|
588
|
-
|
|
589
|
-
|
|
788
|
+
this._reconnectTimer = setTimeout(() => {
|
|
789
|
+
this._reconnectTimer = null;
|
|
790
|
+
this.connect().catch(() => {
|
|
791
|
+
});
|
|
792
|
+
}, delay);
|
|
590
793
|
} else {
|
|
591
794
|
console.error("[Dolphin] Max reconnect attempts reached.");
|
|
592
795
|
}
|
|
@@ -764,6 +967,7 @@ var DolphinClient = class {
|
|
|
764
967
|
|
|
765
968
|
// src/dom.ts
|
|
766
969
|
function attachDOMBinding(clientProto) {
|
|
970
|
+
const componentPromiseCache = /* @__PURE__ */ new Map();
|
|
767
971
|
function escapeRegExp(str) {
|
|
768
972
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
769
973
|
}
|
|
@@ -1010,7 +1214,9 @@ function attachDOMBinding(clientProto) {
|
|
|
1010
1214
|
const scheduleFn = typeof requestAnimationFrame !== "undefined" ? requestAnimationFrame : (cb) => setTimeout(cb, 0);
|
|
1011
1215
|
scheduleFn(() => {
|
|
1012
1216
|
pendingUpdates.forEach((html, el) => {
|
|
1013
|
-
|
|
1217
|
+
if (el.isConnected !== false) {
|
|
1218
|
+
patchDOM(el, html);
|
|
1219
|
+
}
|
|
1014
1220
|
});
|
|
1015
1221
|
pendingUpdates.clear();
|
|
1016
1222
|
rafScheduled = false;
|
|
@@ -1149,7 +1355,7 @@ function attachDOMBinding(clientProto) {
|
|
|
1149
1355
|
if (this._domInitialized) return;
|
|
1150
1356
|
this._domInitialized = true;
|
|
1151
1357
|
const PUSH_EVENTS = ["input", "change", "keyup", "paste", "blur"];
|
|
1152
|
-
const debounceTimers = /* @__PURE__ */ new
|
|
1358
|
+
const debounceTimers = /* @__PURE__ */ new WeakMap();
|
|
1153
1359
|
PUSH_EVENTS.forEach((evtName) => {
|
|
1154
1360
|
this.addDomListener(document, evtName, (e) => {
|
|
1155
1361
|
if (!e.target || !e.target.getAttribute) return;
|
|
@@ -1202,6 +1408,14 @@ function attachDOMBinding(clientProto) {
|
|
|
1202
1408
|
const rtTopic = e.target.getAttribute("data-rt-submit");
|
|
1203
1409
|
const apiTarget = e.target.getAttribute("data-api-submit");
|
|
1204
1410
|
if (rtTopic || apiTarget) {
|
|
1411
|
+
const formInputs = e.target.querySelectorAll("[name]");
|
|
1412
|
+
formInputs.forEach((inputEl) => {
|
|
1413
|
+
const name = inputEl.name;
|
|
1414
|
+
if (name) {
|
|
1415
|
+
this.publish(`errors/${name}`, "");
|
|
1416
|
+
inputEl.classList.remove("invalid");
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1205
1419
|
const validatedInputs = e.target.querySelectorAll("[data-rt-validate]");
|
|
1206
1420
|
let formIsValid = true;
|
|
1207
1421
|
if (validatedInputs.length > 0 && typeof this.validateField === "function") {
|
|
@@ -1215,9 +1429,6 @@ function attachDOMBinding(clientProto) {
|
|
|
1215
1429
|
formIsValid = false;
|
|
1216
1430
|
inputEl.classList.add("invalid");
|
|
1217
1431
|
this.publish(`errors/${name}`, errorMsg);
|
|
1218
|
-
} else {
|
|
1219
|
-
inputEl.classList.remove("invalid");
|
|
1220
|
-
this.publish(`errors/${name}`, "");
|
|
1221
1432
|
}
|
|
1222
1433
|
}
|
|
1223
1434
|
});
|
|
@@ -1245,8 +1456,11 @@ function attachDOMBinding(clientProto) {
|
|
|
1245
1456
|
resolvedTarget = resolvedTarget.replace(new RegExp(`\\{\\{${escapedK}\\}\\}`, "g"), parentCtx[k] !== void 0 && parentCtx[k] !== null ? parentCtx[k] : "");
|
|
1246
1457
|
}
|
|
1247
1458
|
const parts = resolvedTarget.trim().split(" ");
|
|
1248
|
-
|
|
1459
|
+
let method = parts.length > 1 ? parts[0].toUpperCase() : "POST";
|
|
1249
1460
|
const path = parts.length > 1 ? parts[1] : parts[0];
|
|
1461
|
+
if (data._method) {
|
|
1462
|
+
method = String(data._method).toUpperCase();
|
|
1463
|
+
}
|
|
1250
1464
|
try {
|
|
1251
1465
|
const result = await this.api.request(method, path, data);
|
|
1252
1466
|
const resultBind = e.target.getAttribute("data-api-result");
|
|
@@ -1333,6 +1547,8 @@ function attachDOMBinding(clientProto) {
|
|
|
1333
1547
|
});
|
|
1334
1548
|
this._scanAndFetchAPIBinds();
|
|
1335
1549
|
this._scanStoreBinds();
|
|
1550
|
+
this._resolveImports();
|
|
1551
|
+
this._initSPARouter();
|
|
1336
1552
|
};
|
|
1337
1553
|
clientProto._scanAndFetchAPIBinds = async function() {
|
|
1338
1554
|
if (typeof document === "undefined") return;
|
|
@@ -1340,6 +1556,10 @@ function attachDOMBinding(clientProto) {
|
|
|
1340
1556
|
for (const el of Array.from(elements)) {
|
|
1341
1557
|
const path = el.getAttribute("data-api-get");
|
|
1342
1558
|
if (!path) continue;
|
|
1559
|
+
if (typeof el.hasAttribute === "function" && el.hasAttribute("data-api-initialized")) continue;
|
|
1560
|
+
if (typeof el.setAttribute === "function") {
|
|
1561
|
+
el.setAttribute("data-api-initialized", "true");
|
|
1562
|
+
}
|
|
1343
1563
|
try {
|
|
1344
1564
|
const result = await this.api.get(path);
|
|
1345
1565
|
const apiStore = el.getAttribute("data-api-store");
|
|
@@ -1369,7 +1589,8 @@ function attachDOMBinding(clientProto) {
|
|
|
1369
1589
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1370
1590
|
el.value = typeof result === "object" ? result.value !== void 0 ? result.value : "" : result;
|
|
1371
1591
|
} else {
|
|
1372
|
-
|
|
1592
|
+
const rawHTML = typeof result === "object" ? result.html || result.text || JSON.stringify(result) : String(result);
|
|
1593
|
+
el.innerHTML = sanitizeHTML(rawHTML);
|
|
1373
1594
|
}
|
|
1374
1595
|
}
|
|
1375
1596
|
}
|
|
@@ -1586,11 +1807,158 @@ function attachDOMBinding(clientProto) {
|
|
|
1586
1807
|
}
|
|
1587
1808
|
if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
|
|
1588
1809
|
el.value = typeof processedPayload === "object" ? processedPayload.value !== void 0 ? processedPayload.value : "" : processedPayload;
|
|
1810
|
+
} else if (template) {
|
|
1811
|
+
el.innerHTML = typeof processedPayload === "object" ? processedPayload.html || processedPayload.text || JSON.stringify(processedPayload) : String(processedPayload);
|
|
1589
1812
|
} else {
|
|
1590
|
-
|
|
1813
|
+
const rawHTML = typeof processedPayload === "object" ? processedPayload.html || processedPayload.text || JSON.stringify(processedPayload) : String(processedPayload);
|
|
1814
|
+
el.innerHTML = sanitizeHTML(rawHTML);
|
|
1591
1815
|
}
|
|
1592
1816
|
});
|
|
1593
1817
|
};
|
|
1818
|
+
clientProto._resolveImports = async function(container) {
|
|
1819
|
+
if (typeof document === "undefined") return;
|
|
1820
|
+
const root = container || document.body || document;
|
|
1821
|
+
if (!root || typeof root.querySelectorAll !== "function") return;
|
|
1822
|
+
const elements = root.querySelectorAll("[data-import]");
|
|
1823
|
+
if (elements.length === 0) return;
|
|
1824
|
+
const resolveNode = async (el, resolvingSet) => {
|
|
1825
|
+
const src = el.getAttribute("data-import");
|
|
1826
|
+
if (!src) return;
|
|
1827
|
+
if (resolvingSet.has(src)) {
|
|
1828
|
+
console.warn(`[Dolphin Component Warning]: Circular import detected for "${src}". Skipping resolving.`);
|
|
1829
|
+
el.innerHTML = `<span style="color:red;font-weight:bold;">Circular import: ${src}</span>`;
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
resolvingSet.add(src);
|
|
1833
|
+
let promise = componentPromiseCache.get(src);
|
|
1834
|
+
if (!promise) {
|
|
1835
|
+
promise = fetch(src).then((res) => {
|
|
1836
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1837
|
+
return res.text();
|
|
1838
|
+
});
|
|
1839
|
+
promise.catch(() => componentPromiseCache.delete(src));
|
|
1840
|
+
componentPromiseCache.set(src, promise);
|
|
1841
|
+
}
|
|
1842
|
+
let content = "";
|
|
1843
|
+
try {
|
|
1844
|
+
content = await promise;
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
console.error(`[Dolphin Component Error]: Failed to fetch component "${src}":`, err);
|
|
1847
|
+
content = `<span style="color:red;font-weight:bold;">Failed to import ${src}</span>`;
|
|
1848
|
+
}
|
|
1849
|
+
el.innerHTML = sanitizeHTML(content);
|
|
1850
|
+
el.removeAttribute("data-import");
|
|
1851
|
+
const nestedElements = el.querySelectorAll("[data-import]");
|
|
1852
|
+
if (nestedElements.length > 0) {
|
|
1853
|
+
const subPromises = Array.from(nestedElements).map((child) => resolveNode(child, new Set(resolvingSet)));
|
|
1854
|
+
await Promise.all(subPromises);
|
|
1855
|
+
}
|
|
1856
|
+
this._scanStoreBinds();
|
|
1857
|
+
this._scanAndFetchAPIBinds();
|
|
1858
|
+
};
|
|
1859
|
+
const promises = Array.from(elements).map((el) => resolveNode(el, /* @__PURE__ */ new Set()));
|
|
1860
|
+
await Promise.all(promises);
|
|
1861
|
+
};
|
|
1862
|
+
clientProto._initSPARouter = function() {
|
|
1863
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1864
|
+
if (this._routerInitialized) return;
|
|
1865
|
+
this._routerInitialized = true;
|
|
1866
|
+
let _spaAbortController = null;
|
|
1867
|
+
const findViewport = () => {
|
|
1868
|
+
const selector = this.options.routerViewport || "main, #viewport, body";
|
|
1869
|
+
const selectors = selector.split(",").map((s) => s.trim());
|
|
1870
|
+
for (const sel of selectors) {
|
|
1871
|
+
const el = document.querySelector(sel);
|
|
1872
|
+
if (el) return el;
|
|
1873
|
+
}
|
|
1874
|
+
return document.body;
|
|
1875
|
+
};
|
|
1876
|
+
const loadPage = async (url, pushState = true) => {
|
|
1877
|
+
try {
|
|
1878
|
+
if (this.options.debug) {
|
|
1879
|
+
console.log(`%c\u{1F6E3}\uFE0F [Dolphin Router]: Navigating to ${url}...`, "color: #3b82f6; font-weight: bold;");
|
|
1880
|
+
}
|
|
1881
|
+
if (_spaAbortController) {
|
|
1882
|
+
_spaAbortController.abort();
|
|
1883
|
+
}
|
|
1884
|
+
_spaAbortController = new AbortController();
|
|
1885
|
+
const signal = _spaAbortController.signal;
|
|
1886
|
+
const viewport = findViewport();
|
|
1887
|
+
if (this.options.routerTransitions && viewport) {
|
|
1888
|
+
viewport.classList.add("dolphin-fade-out");
|
|
1889
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1890
|
+
}
|
|
1891
|
+
const response = await fetch(url, { signal });
|
|
1892
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
1893
|
+
const html = await response.text();
|
|
1894
|
+
_spaAbortController = null;
|
|
1895
|
+
const parser = new DOMParser();
|
|
1896
|
+
const doc = parser.parseFromString(html, "text/html");
|
|
1897
|
+
if (doc.title) {
|
|
1898
|
+
document.title = doc.title;
|
|
1899
|
+
}
|
|
1900
|
+
const newViewport = doc.querySelector(this.options.routerViewport || "main, #viewport, body");
|
|
1901
|
+
const currentViewport = findViewport();
|
|
1902
|
+
if (newViewport && currentViewport) {
|
|
1903
|
+
currentViewport.innerHTML = newViewport.innerHTML;
|
|
1904
|
+
Array.from(newViewport.attributes).forEach((attr) => {
|
|
1905
|
+
currentViewport.setAttribute(attr.name, attr.value);
|
|
1906
|
+
});
|
|
1907
|
+
} else if (currentViewport) {
|
|
1908
|
+
currentViewport.innerHTML = doc.body.innerHTML;
|
|
1909
|
+
}
|
|
1910
|
+
if (pushState) {
|
|
1911
|
+
window.history.pushState({ dolphinSpa: true, url }, "", url);
|
|
1912
|
+
}
|
|
1913
|
+
if (this.options.routerTransitions && currentViewport) {
|
|
1914
|
+
currentViewport.classList.remove("dolphin-fade-out");
|
|
1915
|
+
currentViewport.classList.add("dolphin-fade-in");
|
|
1916
|
+
setTimeout(() => currentViewport.classList.remove("dolphin-fade-in"), 300);
|
|
1917
|
+
}
|
|
1918
|
+
await this._resolveImports(currentViewport);
|
|
1919
|
+
this._scanStoreBinds();
|
|
1920
|
+
this._scanAndFetchAPIBinds();
|
|
1921
|
+
} catch (err) {
|
|
1922
|
+
if (err && err.name === "AbortError") return;
|
|
1923
|
+
console.error("[Dolphin Router Error]: Failed to route page:", err);
|
|
1924
|
+
window.location.href = url;
|
|
1925
|
+
}
|
|
1926
|
+
};
|
|
1927
|
+
this.addDomListener(document, "click", (e) => {
|
|
1928
|
+
const anchor = e.target.closest("a");
|
|
1929
|
+
if (!anchor) return;
|
|
1930
|
+
if (!anchor.hasAttribute("data-spa") && anchor.getAttribute("data-spa") !== "true") return;
|
|
1931
|
+
const href = anchor.getAttribute("href");
|
|
1932
|
+
if (!href || href.startsWith("#") || href.startsWith("javascript:") || href.startsWith("mailto:") || href.startsWith("tel:")) return;
|
|
1933
|
+
const url = new URL(href, window.location.href);
|
|
1934
|
+
if (url.origin !== window.location.origin) return;
|
|
1935
|
+
e.preventDefault();
|
|
1936
|
+
loadPage(href);
|
|
1937
|
+
});
|
|
1938
|
+
this.addDomListener(window, "popstate", (e) => {
|
|
1939
|
+
if (e.state && e.state.dolphinSpa) {
|
|
1940
|
+
loadPage(e.state.url, false);
|
|
1941
|
+
} else if (e.state === null) {
|
|
1942
|
+
loadPage(window.location.pathname, false);
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
if (this.options.routerTransitions) {
|
|
1946
|
+
const style = document.createElement("style");
|
|
1947
|
+
style.innerHTML = `
|
|
1948
|
+
.dolphin-fade-out {
|
|
1949
|
+
opacity: 0;
|
|
1950
|
+
transition: opacity 0.15s ease-in-out;
|
|
1951
|
+
}
|
|
1952
|
+
.dolphin-fade-in {
|
|
1953
|
+
opacity: 0;
|
|
1954
|
+
}
|
|
1955
|
+
main, #viewport, body {
|
|
1956
|
+
transition: opacity 0.15s ease-in-out;
|
|
1957
|
+
}
|
|
1958
|
+
`;
|
|
1959
|
+
document.head.appendChild(style);
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1594
1962
|
}
|
|
1595
1963
|
|
|
1596
1964
|
// src/offline.ts
|
|
@@ -1668,6 +2036,10 @@ var DolphinOffline = class {
|
|
|
1668
2036
|
const store = transaction.objectStore("cache");
|
|
1669
2037
|
store.put({ data, timestamp: Date.now() }, key);
|
|
1670
2038
|
transaction.oncomplete = () => resolve();
|
|
2039
|
+
transaction.onerror = () => {
|
|
2040
|
+
console.warn("[Dolphin Offline] setCache write failed for key:", key);
|
|
2041
|
+
resolve();
|
|
2042
|
+
};
|
|
1671
2043
|
} catch {
|
|
1672
2044
|
resolve();
|
|
1673
2045
|
}
|
|
@@ -1690,6 +2062,11 @@ var DolphinOffline = class {
|
|
|
1690
2062
|
const store = transaction.objectStore("mutations");
|
|
1691
2063
|
store.add(mutation);
|
|
1692
2064
|
transaction.oncomplete = () => resolve();
|
|
2065
|
+
transaction.onerror = () => {
|
|
2066
|
+
console.warn("[Dolphin Offline] queueMutation write failed:", method, path);
|
|
2067
|
+
this.memoryMutations.push(mutation);
|
|
2068
|
+
resolve();
|
|
2069
|
+
};
|
|
1693
2070
|
} catch {
|
|
1694
2071
|
resolve();
|
|
1695
2072
|
}
|
|
@@ -1722,6 +2099,10 @@ var DolphinOffline = class {
|
|
|
1722
2099
|
const store = transaction.objectStore("mutations");
|
|
1723
2100
|
store.delete(id);
|
|
1724
2101
|
transaction.oncomplete = () => resolve();
|
|
2102
|
+
transaction.onerror = () => {
|
|
2103
|
+
console.warn("[Dolphin Offline] removeMutation failed for id:", id);
|
|
2104
|
+
resolve();
|
|
2105
|
+
};
|
|
1725
2106
|
} catch {
|
|
1726
2107
|
resolve();
|
|
1727
2108
|
}
|
|
@@ -2067,6 +2448,9 @@ function attachDragDrop(clientProto) {
|
|
|
2067
2448
|
function attachCollab(clientProto) {
|
|
2068
2449
|
clientProto._initCollab = function() {
|
|
2069
2450
|
if (typeof document === "undefined") return;
|
|
2451
|
+
const remoteCursors = /* @__PURE__ */ new Map();
|
|
2452
|
+
const cursorStaleTimers = /* @__PURE__ */ new Map();
|
|
2453
|
+
const CURSOR_STALE_MS = 5e3;
|
|
2070
2454
|
this.addDomListener(document, "mousemove", (e) => {
|
|
2071
2455
|
const shareContainers = document.querySelectorAll("[data-rt-cursor-share]");
|
|
2072
2456
|
shareContainers.forEach((container) => {
|
|
@@ -2103,6 +2487,7 @@ function attachCollab(clientProto) {
|
|
|
2103
2487
|
if (e.target._typingTimer) clearTimeout(e.target._typingTimer);
|
|
2104
2488
|
e.target._typingTimer = setTimeout(() => {
|
|
2105
2489
|
e.target._isTyping = false;
|
|
2490
|
+
e.target._typingTimer = null;
|
|
2106
2491
|
publishTyping(false);
|
|
2107
2492
|
}, 2e3);
|
|
2108
2493
|
});
|
|
@@ -2126,21 +2511,32 @@ function attachCollab(clientProto) {
|
|
|
2126
2511
|
if (remoteDeviceId === this.deviceId) return;
|
|
2127
2512
|
const container = document.querySelector(`[data-rt-cursor-share="${room}"]`);
|
|
2128
2513
|
if (!container) return;
|
|
2129
|
-
|
|
2130
|
-
|
|
2514
|
+
const cursorKey = `${room}::${remoteDeviceId}`;
|
|
2515
|
+
let cursorEl = remoteCursors.get(cursorKey);
|
|
2516
|
+
if (!cursorEl || !document.contains(cursorEl)) {
|
|
2131
2517
|
cursorEl = document.createElement("div");
|
|
2132
2518
|
cursorEl.className = `rt-cursor rt-cursor-${remoteDeviceId}`;
|
|
2133
2519
|
cursorEl.style.position = "absolute";
|
|
2134
2520
|
cursorEl.style.width = "10px";
|
|
2135
2521
|
cursorEl.style.height = "10px";
|
|
2136
2522
|
cursorEl.style.borderRadius = "50%";
|
|
2137
|
-
cursorEl.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16);
|
|
2523
|
+
cursorEl.style.backgroundColor = "#" + Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0");
|
|
2138
2524
|
cursorEl.style.pointerEvents = "none";
|
|
2139
2525
|
container.appendChild(cursorEl);
|
|
2526
|
+
remoteCursors.set(cursorKey, cursorEl);
|
|
2140
2527
|
}
|
|
2141
2528
|
const box = container.getBoundingClientRect();
|
|
2142
2529
|
cursorEl.style.left = payload.x * box.width + "px";
|
|
2143
2530
|
cursorEl.style.top = payload.y * box.height + "px";
|
|
2531
|
+
if (cursorStaleTimers.has(cursorKey)) {
|
|
2532
|
+
clearTimeout(cursorStaleTimers.get(cursorKey));
|
|
2533
|
+
}
|
|
2534
|
+
cursorStaleTimers.set(cursorKey, setTimeout(() => {
|
|
2535
|
+
const el = remoteCursors.get(cursorKey);
|
|
2536
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
2537
|
+
remoteCursors.delete(cursorKey);
|
|
2538
|
+
cursorStaleTimers.delete(cursorKey);
|
|
2539
|
+
}, CURSOR_STALE_MS));
|
|
2144
2540
|
});
|
|
2145
2541
|
this.subscribe("collab/+/crdt", (payload, topic) => {
|
|
2146
2542
|
if (payload.deviceId === this.deviceId) return;
|
|
@@ -2158,6 +2554,14 @@ function attachCollab(clientProto) {
|
|
|
2158
2554
|
}
|
|
2159
2555
|
});
|
|
2160
2556
|
});
|
|
2557
|
+
this._collabCleanup = () => {
|
|
2558
|
+
cursorStaleTimers.forEach((t) => clearTimeout(t));
|
|
2559
|
+
cursorStaleTimers.clear();
|
|
2560
|
+
remoteCursors.forEach((el) => {
|
|
2561
|
+
if (el && el.parentNode) el.parentNode.removeChild(el);
|
|
2562
|
+
});
|
|
2563
|
+
remoteCursors.clear();
|
|
2564
|
+
};
|
|
2161
2565
|
};
|
|
2162
2566
|
}
|
|
2163
2567
|
|