@panphora/clayjs 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -5
- package/_headers +12 -0
- package/clay-events.js +9 -0
- package/package.json +1 -1
- package/src/core/host-meta.js +107 -0
- package/src/core/save.js +55 -2
- package/src/lib/dirty-gate.js +20 -3
- package/src/loader-logic.js +23 -1
- package/src/loader.js +6 -0
- package/src/plugins/upload.js +185 -0
- package/src/plugins/wire.js +598 -0
- package/src/sync/live-sync.js +55 -7
- package/src/vendor/hypercms.vendor.js +53 -11
- package/src/vendor/quickcrop.vendor.js +401 -0
package/src/sync/live-sync.js
CHANGED
|
@@ -423,6 +423,34 @@ class LiveSync {
|
|
|
423
423
|
if (this.onConnect) this.onConnect();
|
|
424
424
|
};
|
|
425
425
|
|
|
426
|
+
// The cursor frame is a NAMED SSE event, so it never reaches onmessage and
|
|
427
|
+
// never looks like data. Its `resync` flag says the server could not retain
|
|
428
|
+
// everything between where this client resumed and the baseline it is
|
|
429
|
+
// sending: what this page holds is stale in a way no replay will fix.
|
|
430
|
+
//
|
|
431
|
+
// The repair is the token-free fetch of the served document this class
|
|
432
|
+
// already runs for a change too large to send, so noticing the flag is the
|
|
433
|
+
// whole of the work.
|
|
434
|
+
this.sse.addEventListener('cursor', (event) => {
|
|
435
|
+
let data;
|
|
436
|
+
try {
|
|
437
|
+
data = JSON.parse(event.data);
|
|
438
|
+
} catch {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!data || data.resync !== true) return;
|
|
442
|
+
console.log('[LiveSync] Server could not replay everything; refetching the document');
|
|
443
|
+
// _fetchServedDocument, deliberately, and not _fetchExternalChange: that
|
|
444
|
+
// one drops a fetch whose seq is at or below the external watermark, and
|
|
445
|
+
// the cursor baseline routinely is, since it is the server's high-water
|
|
446
|
+
// mark and our own last applied change may already have reached it. A
|
|
447
|
+
// resync that skipped itself for being "already seen" would leave the page
|
|
448
|
+
// permanently stale, which is the exact failure the flag exists to report.
|
|
449
|
+
this._fetchServedDocument(typeof data.seq === 'number' ? data.seq : undefined, {
|
|
450
|
+
repair: true,
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
426
454
|
this.sse.onmessage = (event) => {
|
|
427
455
|
const data = JSON.parse(event.data);
|
|
428
456
|
|
|
@@ -654,20 +682,29 @@ class LiveSync {
|
|
|
654
682
|
* frame the epoch check refused (the fetched body is whatever disk holds
|
|
655
683
|
* NOW, which is always safe to apply).
|
|
656
684
|
*/
|
|
657
|
-
_fetchServedDocument(seq, attempt = 0) {
|
|
685
|
+
_fetchServedDocument(seq, { attempt = 0, repair = false } = {}) {
|
|
658
686
|
const epoch = this._saveEpoch;
|
|
659
687
|
fetch(new URL(window.location.href), { cache: 'no-store' })
|
|
660
688
|
.then((response) => (response.ok ? response.text() : null))
|
|
661
689
|
.then((html) => {
|
|
662
690
|
if (this.isDestroyed || html == null) return;
|
|
663
|
-
if (typeof seq === 'number' && seq < this._lastExternalSeq)
|
|
691
|
+
if (typeof seq === 'number' && seq < this._lastExternalSeq) {
|
|
692
|
+
// A newer external change superseded this one, and its own fetch will
|
|
693
|
+
// queue a body. Except for a repair: that one exists because the server
|
|
694
|
+
// said replay cannot fix this page, so if the newer fetch fails there is
|
|
695
|
+
// nothing else coming. Refetch rather than drop the only repair.
|
|
696
|
+
if (repair && attempt < 3) {
|
|
697
|
+
this._fetchServedDocument(seq, { attempt: attempt + 1, repair });
|
|
698
|
+
}
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
664
701
|
if (this._saveEpoch > epoch) {
|
|
665
702
|
// An own save landed while the GET was in flight, so this body may
|
|
666
703
|
// predate it. Save-response order proves nothing about disk-write
|
|
667
704
|
// order — refetch for the newest bytes instead of dropping.
|
|
668
705
|
if (attempt < 3) {
|
|
669
706
|
console.log('[LiveSync] Refetching external change: own save landed mid-fetch');
|
|
670
|
-
this._fetchServedDocument(seq, attempt + 1);
|
|
707
|
+
this._fetchServedDocument(seq, { attempt: attempt + 1, repair });
|
|
671
708
|
}
|
|
672
709
|
return;
|
|
673
710
|
}
|
|
@@ -989,8 +1026,12 @@ class LiveSync {
|
|
|
989
1026
|
// onmessage before applyUpdate is ever called. Covers every SSE morph
|
|
990
1027
|
// source (peer edit, version restore, body-swap) since they all funnel
|
|
991
1028
|
// through this single choke point.
|
|
1029
|
+
//
|
|
1030
|
+
// `source` is what lets a listener tell the two apply paths apart. clay.wire
|
|
1031
|
+
// waits for a DISK frame to call an agent's write landed, and another tab's
|
|
1032
|
+
// edit arriving first would otherwise report the wrong bytes as delivered.
|
|
992
1033
|
document.dispatchEvent(new CustomEvent('clay:sync-applied', {
|
|
993
|
-
detail: { seq }
|
|
1034
|
+
detail: { seq, source: 'peer' }
|
|
994
1035
|
}));
|
|
995
1036
|
} finally {
|
|
996
1037
|
this._log('applyUpdate - morph complete, resuming mutations');
|
|
@@ -1042,7 +1083,14 @@ class LiveSync {
|
|
|
1042
1083
|
const parser = new DOMParser();
|
|
1043
1084
|
const newDoc = parser.parseFromString(html, 'text/html');
|
|
1044
1085
|
|
|
1045
|
-
|
|
1086
|
+
// Lane-guarded exactly like the peer path. A view-mode tab has no save
|
|
1087
|
+
// baseline — every writer of lastSavedContents is edit-gated — so
|
|
1088
|
+
// protectDiskDoc can only ever refuse, and the frame would hold, retry
|
|
1089
|
+
// every 3s, and hold again forever. The gate still reads dirty there,
|
|
1090
|
+
// because persistProbeDirty inspects the live DOM and a visitor can type
|
|
1091
|
+
// into a [persist] field. Before the resync repair this path was
|
|
1092
|
+
// unreachable outside the live lane; now it is the repair's own route.
|
|
1093
|
+
if (this.lane === 'live' && pageMaybeDirty()) {
|
|
1046
1094
|
const protection = protectDiskDoc({ newDoc });
|
|
1047
1095
|
if (!protection.ok) {
|
|
1048
1096
|
// Hold: nothing morphs, no baseline moves, and deliberately NO
|
|
@@ -1094,7 +1142,7 @@ class LiveSync {
|
|
|
1094
1142
|
|
|
1095
1143
|
window.scrollTo(scrollX, scrollY);
|
|
1096
1144
|
|
|
1097
|
-
if (retainedRoots === 0 && !pageMaybeDirty()) {
|
|
1145
|
+
if (this.lane === 'live' && retainedRoots === 0 && !pageMaybeDirty()) {
|
|
1098
1146
|
// Clean apply: the DOM now IS the disk state, so a local comparison
|
|
1099
1147
|
// capture of it is the truthful baseline. The next no-op save skips,
|
|
1100
1148
|
// beforeunload stays quiet. The dirty re-check matters: typing that
|
|
@@ -1115,7 +1163,7 @@ class LiveSync {
|
|
|
1115
1163
|
}
|
|
1116
1164
|
|
|
1117
1165
|
document.dispatchEvent(new CustomEvent('clay:sync-applied', {
|
|
1118
|
-
detail: { seq }
|
|
1166
|
+
detail: { seq, source: 'disk' }
|
|
1119
1167
|
}));
|
|
1120
1168
|
} finally {
|
|
1121
1169
|
this._log('applyExternal - morph complete, resuming mutations');
|