@panphora/clayjs 1.5.2 → 1.5.3
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/dist/clay.standalone.js +57 -9
- package/package.json +1 -1
- package/src/core/save.js +7 -0
- package/src/lib/user-gesture.js +10 -0
- package/src/sync/live-sync.js +62 -11
package/dist/clay.standalone.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* clayjs 1.5.
|
|
1
|
+
/* clayjs 1.5.3 standalone build: every clayjs module in one file. https://clayjs.com/offline
|
|
2
2
|
Third-party code inside keeps its own license (Sortable MIT, Squire MIT, DOMPurify Apache-2.0 OR MPL-2.0, MicroModal MIT, parse5 MIT): https://clayjs.com/THIRD-PARTY-NOTICES.md */
|
|
3
3
|
(() => {
|
|
4
4
|
var __create = Object.create;
|
|
@@ -541,6 +541,7 @@
|
|
|
541
541
|
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
542
542
|
}
|
|
543
543
|
function markGestureTurn() {
|
|
544
|
+
anyGesture = true;
|
|
544
545
|
gestureTaskActive = true;
|
|
545
546
|
lastTrustedGestureTs = now();
|
|
546
547
|
setTimeout(() => {
|
|
@@ -565,6 +566,9 @@
|
|
|
565
566
|
function markUserDriven() {
|
|
566
567
|
userDrivenSinceLastSave = true;
|
|
567
568
|
}
|
|
569
|
+
function gestureSeen() {
|
|
570
|
+
return anyGesture;
|
|
571
|
+
}
|
|
568
572
|
function consumeUserDriven() {
|
|
569
573
|
const v2 = userDrivenSinceLastSave;
|
|
570
574
|
userDrivenSinceLastSave = false;
|
|
@@ -581,13 +585,14 @@
|
|
|
581
585
|
function clearExplicitSave() {
|
|
582
586
|
explicitSaveIntent = false;
|
|
583
587
|
}
|
|
584
|
-
var GESTURE_EVENTS, RECENT_GESTURE_MS, gestureTaskActive, lastTrustedGestureTs, userDrivenSinceLastSave, explicitSaveIntent, installed;
|
|
588
|
+
var GESTURE_EVENTS, RECENT_GESTURE_MS, gestureTaskActive, lastTrustedGestureTs, anyGesture, userDrivenSinceLastSave, explicitSaveIntent, installed;
|
|
585
589
|
var init_user_gesture = __esm({
|
|
586
590
|
"src/lib/user-gesture.js"() {
|
|
587
591
|
GESTURE_EVENTS = ["pointerdown", "pointerup", "click", "keydown", "beforeinput", "change", "submit", "paste", "drop", "cut"];
|
|
588
592
|
RECENT_GESTURE_MS = 500;
|
|
589
593
|
gestureTaskActive = false;
|
|
590
594
|
lastTrustedGestureTs = -Infinity;
|
|
595
|
+
anyGesture = false;
|
|
591
596
|
userDrivenSinceLastSave = false;
|
|
592
597
|
explicitSaveIntent = false;
|
|
593
598
|
installed = false;
|
|
@@ -2497,6 +2502,7 @@
|
|
|
2497
2502
|
var save_exports = {};
|
|
2498
2503
|
__export(save_exports, {
|
|
2499
2504
|
addDocumentTransform: () => addDocumentTransform,
|
|
2505
|
+
baselineSettled: () => baselineSettled,
|
|
2500
2506
|
conflictResolvedBySync: () => conflictResolvedBySync,
|
|
2501
2507
|
default: () => save_default,
|
|
2502
2508
|
getLastSavedBytes: () => getLastSavedBytes,
|
|
@@ -2754,6 +2760,9 @@
|
|
|
2754
2760
|
}
|
|
2755
2761
|
});
|
|
2756
2762
|
}
|
|
2763
|
+
function baselineSettled() {
|
|
2764
|
+
return baselineSettledAt;
|
|
2765
|
+
}
|
|
2757
2766
|
function initBaselineCapture() {
|
|
2758
2767
|
if (!isEditMode) return;
|
|
2759
2768
|
let userEdited = false;
|
|
@@ -2789,6 +2798,8 @@
|
|
|
2789
2798
|
}
|
|
2790
2799
|
baselineActive = false;
|
|
2791
2800
|
document.documentElement.setAttribute("savestatus", "saved");
|
|
2801
|
+
baselineSettledAt = true;
|
|
2802
|
+
document.dispatchEvent(new CustomEvent("clay:baseline-settled"));
|
|
2792
2803
|
};
|
|
2793
2804
|
unsubscribeMutation = mutation_default.onAnyChange(
|
|
2794
2805
|
{ debounce: SETTLE_MS, omitChangeDetails: true, require: "autosave" },
|
|
@@ -2856,7 +2867,7 @@
|
|
|
2856
2867
|
initSaveKeyboardShortcut();
|
|
2857
2868
|
initHyperclaySaveButton();
|
|
2858
2869
|
}
|
|
2859
|
-
var savingTimeout, unsavedChanges, lastSavedContents, lastSavedDirty, pendingSave, autosaveSuspended, autosaveMissed, conflictHold, conflictEtag, lastSavedBytes, throttledSave, baselineContents, baselineActive, SETTLE_MS, MAX_SETTLE_MS, save_default;
|
|
2870
|
+
var savingTimeout, unsavedChanges, lastSavedContents, lastSavedDirty, pendingSave, autosaveSuspended, autosaveMissed, conflictHold, conflictEtag, lastSavedBytes, throttledSave, baselineContents, baselineActive, baselineSettledAt, SETTLE_MS, MAX_SETTLE_MS, save_default;
|
|
2860
2871
|
var init_save = __esm({
|
|
2861
2872
|
"src/core/save.js"() {
|
|
2862
2873
|
init_throttle();
|
|
@@ -2900,6 +2911,7 @@
|
|
|
2900
2911
|
throttledSave = throttle_default(savePage, 1200);
|
|
2901
2912
|
baselineContents = "";
|
|
2902
2913
|
baselineActive = true;
|
|
2914
|
+
baselineSettledAt = false;
|
|
2903
2915
|
SETTLE_MS = 500;
|
|
2904
2916
|
MAX_SETTLE_MS = 3e3;
|
|
2905
2917
|
if (document.readyState === "loading") {
|
|
@@ -12183,6 +12195,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
12183
12195
|
init_host_meta();
|
|
12184
12196
|
init_etag();
|
|
12185
12197
|
init_dirty_gate();
|
|
12198
|
+
init_user_gesture();
|
|
12186
12199
|
init_stream();
|
|
12187
12200
|
init_save();
|
|
12188
12201
|
({ createIdentityStore } = HyperMorph);
|
|
@@ -12274,6 +12287,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
12274
12287
|
this._saveEpoch = 0;
|
|
12275
12288
|
this._saveSavedHandler = null;
|
|
12276
12289
|
this._saveConflictHandler = null;
|
|
12290
|
+
this._settledHandler = null;
|
|
12277
12291
|
this._lastIdentityMap = null;
|
|
12278
12292
|
this._applyGen = 0;
|
|
12279
12293
|
this._holdRetryPeer = null;
|
|
@@ -12321,6 +12335,18 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
12321
12335
|
return Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
12322
12336
|
}
|
|
12323
12337
|
}
|
|
12338
|
+
/**
|
|
12339
|
+
* Seed both merge bases (the peer lane's and the disk lane's) with the page as
|
|
12340
|
+
* it stands, so a first frame that arrives after local edits merges instead of
|
|
12341
|
+
* holding. A page already dirty has no trustworthy base; it keeps null and holds.
|
|
12342
|
+
*/
|
|
12343
|
+
_seedBases() {
|
|
12344
|
+
if (pageMaybeDirty()) return;
|
|
12345
|
+
const clone = captureSnapshot({ flushUndo: false });
|
|
12346
|
+
this.lastHtml = serializeForSync(clone);
|
|
12347
|
+
this._lastIdentityMap = this.identity.exportMap(clone, originalSnapshotNode);
|
|
12348
|
+
this._setDiskBase(captureForSaveAndComparison({ emitForSync: false }).forSave, this._ticket());
|
|
12349
|
+
}
|
|
12324
12350
|
/**
|
|
12325
12351
|
* Start the LiveSync system
|
|
12326
12352
|
* Can be called after stop() to restart with a new file
|
|
@@ -12347,14 +12373,32 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
12347
12373
|
this._lastExternalSeq = 0;
|
|
12348
12374
|
this._pendingExternal = null;
|
|
12349
12375
|
this.resumeId = this.generateResumeId();
|
|
12350
|
-
if (this.lane === "live"
|
|
12351
|
-
const clone = captureSnapshot({ flushUndo: false });
|
|
12352
|
-
this.lastHtml = serializeForSync(clone);
|
|
12353
|
-
this._lastIdentityMap = this.identity.exportMap(clone, originalSnapshotNode);
|
|
12354
|
-
this._setDiskBase(captureForSaveAndComparison({ emitForSync: false }).forSave, this._ticket());
|
|
12355
|
-
}
|
|
12376
|
+
if (this.lane === "live") this._seedBases();
|
|
12356
12377
|
console.log(`[LiveSync] Starting for: ${this.currentFile} (lane=${this.lane})`);
|
|
12357
12378
|
const gen = ++this._startGen;
|
|
12379
|
+
if (this.lane === "live" && !baselineSettled()) {
|
|
12380
|
+
const applyGen = this._applyGen;
|
|
12381
|
+
const saveEpoch = this._saveEpoch;
|
|
12382
|
+
const startHtml = this.lastHtml;
|
|
12383
|
+
const startDiskTicket = this._diskBaseTicket;
|
|
12384
|
+
const startDiskSeeded = this._diskBase !== null;
|
|
12385
|
+
this._settledHandler = () => {
|
|
12386
|
+
this._settledHandler = null;
|
|
12387
|
+
if (this.isDestroyed || this._startGen !== gen) return;
|
|
12388
|
+
if (gestureSeen()) return;
|
|
12389
|
+
if (pageMaybeDirty()) return;
|
|
12390
|
+
if (this._saveTicket !== 0) return;
|
|
12391
|
+
if (startHtml !== null && this._applyGen === applyGen && this._saveEpoch === saveEpoch && this.lastHtml === startHtml) {
|
|
12392
|
+
const clone = captureSnapshot({ flushUndo: false });
|
|
12393
|
+
this.lastHtml = serializeForSync(clone);
|
|
12394
|
+
this._lastIdentityMap = this.identity.exportMap(clone, originalSnapshotNode);
|
|
12395
|
+
}
|
|
12396
|
+
if (startDiskSeeded && this._diskBaseTicket === startDiskTicket) {
|
|
12397
|
+
this._setDiskBase(captureForSaveAndComparison({ emitForSync: false }).forSave, this._ticket());
|
|
12398
|
+
}
|
|
12399
|
+
};
|
|
12400
|
+
document.addEventListener("clay:baseline-settled", this._settledHandler, { once: true });
|
|
12401
|
+
}
|
|
12358
12402
|
this._ready = this._resolveProfile().then(() => {
|
|
12359
12403
|
if (this.isDestroyed || this._startGen !== gen) return;
|
|
12360
12404
|
this.connect();
|
|
@@ -12410,6 +12454,10 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
|
|
|
12410
12454
|
document.removeEventListener("clay:save-conflict", this._saveConflictHandler);
|
|
12411
12455
|
this._saveConflictHandler = null;
|
|
12412
12456
|
}
|
|
12457
|
+
if (this._settledHandler) {
|
|
12458
|
+
document.removeEventListener("clay:baseline-settled", this._settledHandler);
|
|
12459
|
+
this._settledHandler = null;
|
|
12460
|
+
}
|
|
12413
12461
|
this._queuedSend = null;
|
|
12414
12462
|
if (this._rafHandle != null) {
|
|
12415
12463
|
this._cancelFrame(this._rafHandle);
|
package/package.json
CHANGED
package/src/core/save.js
CHANGED
|
@@ -572,6 +572,11 @@ let baselineContents = '';
|
|
|
572
572
|
// The baseline veto only guards the load-time settle window; captureBaseline
|
|
573
573
|
// disarms it. See the comment there.
|
|
574
574
|
let baselineActive = true;
|
|
575
|
+
// True once the load-time settle has run. Live sync refreshes an untouched tab's
|
|
576
|
+
// merge bases at this moment: modules that build DOM at boot (an editor mounting
|
|
577
|
+
// from an editmode:resource script) are in the page by now.
|
|
578
|
+
let baselineSettledAt = false;
|
|
579
|
+
export function baselineSettled() { return baselineSettledAt; }
|
|
575
580
|
|
|
576
581
|
// ============================================
|
|
577
582
|
// BASELINE CAPTURE (Settled Signal)
|
|
@@ -658,6 +663,8 @@ function initBaselineCapture() {
|
|
|
658
663
|
baselineActive = false;
|
|
659
664
|
|
|
660
665
|
document.documentElement.setAttribute('savestatus', 'saved');
|
|
666
|
+
baselineSettledAt = true;
|
|
667
|
+
document.dispatchEvent(new CustomEvent('clay:baseline-settled'));
|
|
661
668
|
};
|
|
662
669
|
|
|
663
670
|
// Start settle observer - fires when no mutations for SETTLE_MS.
|
package/src/lib/user-gesture.js
CHANGED
|
@@ -44,6 +44,9 @@ const RECENT_GESTURE_MS = 500;
|
|
|
44
44
|
|
|
45
45
|
let gestureTaskActive = false;
|
|
46
46
|
let lastTrustedGestureTs = -Infinity;
|
|
47
|
+
// Set by the first trusted gesture and never cleared. Live sync reads it at the
|
|
48
|
+
// load-time settle: a page nobody has touched yet holds no one's edit.
|
|
49
|
+
let anyGesture = false;
|
|
47
50
|
let userDrivenSinceLastSave = false;
|
|
48
51
|
let explicitSaveIntent = false;
|
|
49
52
|
let installed = false;
|
|
@@ -55,6 +58,7 @@ function now() {
|
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
function markGestureTurn() {
|
|
61
|
+
anyGesture = true;
|
|
58
62
|
gestureTaskActive = true;
|
|
59
63
|
lastTrustedGestureTs = now();
|
|
60
64
|
// Clear the same-turn flag on the next macrotask. The MutationObserver
|
|
@@ -100,6 +104,11 @@ export function markUserDriven() {
|
|
|
100
104
|
userDrivenSinceLastSave = true;
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
/** True once any trusted gesture has reached the page. */
|
|
108
|
+
export function gestureSeen() {
|
|
109
|
+
return anyGesture;
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
/**
|
|
104
113
|
* Read-and-reset the accumulated bit. Called at the ACTUAL save send (not on a
|
|
105
114
|
* save that never ships), so it survives the autosave debounce and coalescing.
|
|
@@ -144,6 +153,7 @@ export function _resetUserGesture() {
|
|
|
144
153
|
gestureTaskActive = false;
|
|
145
154
|
lastTrustedGestureTs = -Infinity;
|
|
146
155
|
userDrivenSinceLastSave = false;
|
|
156
|
+
anyGesture = false;
|
|
147
157
|
explicitSaveIntent = false;
|
|
148
158
|
}
|
|
149
159
|
|
package/src/sync/live-sync.js
CHANGED
|
@@ -57,6 +57,7 @@ import './section-notice.js';
|
|
|
57
57
|
import { hostMeta } from '../core/host-meta.js';
|
|
58
58
|
import { recordEtag, seedEtag, lastSeenEtag, conditionalSaves } from '../core/etag.js';
|
|
59
59
|
import { pageMaybeDirty, pauseGate, resumeGate, gateCaptureToken, gateClearIfUnchanged, gateMarkDirty } from '../lib/dirty-gate.js';
|
|
60
|
+
import { gestureSeen } from '../lib/user-gesture.js';
|
|
60
61
|
import { SyncStream } from './stream.js';
|
|
61
62
|
|
|
62
63
|
// What a live-sync merge never reads or touches on any side: editor chrome,
|
|
@@ -144,7 +145,7 @@ function pairedBaseline() {
|
|
|
144
145
|
const { forComparison, forDirty } = captureForComparisonAndDirty({ flushUndo: false });
|
|
145
146
|
return [forComparison, forDirty];
|
|
146
147
|
}
|
|
147
|
-
import { savePageThrottled, setLastSavedBaselines, setUnsavedChanges, getLastSavedBytes, conflictResolvedBySync, isSaveConflicted } from '../core/save.js';
|
|
148
|
+
import { savePageThrottled, setLastSavedBaselines, setUnsavedChanges, getLastSavedBytes, conflictResolvedBySync, isSaveConflicted, baselineSettled } from '../core/save.js';
|
|
148
149
|
|
|
149
150
|
/**
|
|
150
151
|
* The two live-sync wires, and the rule for choosing between them.
|
|
@@ -303,6 +304,7 @@ class LiveSync {
|
|
|
303
304
|
this._saveEpoch = 0;
|
|
304
305
|
this._saveSavedHandler = null;
|
|
305
306
|
this._saveConflictHandler = null;
|
|
307
|
+
this._settledHandler = null;
|
|
306
308
|
|
|
307
309
|
// The identityMap that came with lastHtml, so the peer-lane dirty diff
|
|
308
310
|
// can resolve synthetic identities on its base tree.
|
|
@@ -380,6 +382,19 @@ class LiveSync {
|
|
|
380
382
|
}
|
|
381
383
|
}
|
|
382
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Seed both merge bases (the peer lane's and the disk lane's) with the page as
|
|
387
|
+
* it stands, so a first frame that arrives after local edits merges instead of
|
|
388
|
+
* holding. A page already dirty has no trustworthy base; it keeps null and holds.
|
|
389
|
+
*/
|
|
390
|
+
_seedBases() {
|
|
391
|
+
if (pageMaybeDirty()) return;
|
|
392
|
+
const clone = captureSnapshot({ flushUndo: false });
|
|
393
|
+
this.lastHtml = serializeForSync(clone);
|
|
394
|
+
this._lastIdentityMap = this.identity.exportMap(clone, originalSnapshotNode);
|
|
395
|
+
this._setDiskBase(captureForSaveAndComparison({ emitForSync: false }).forSave, this._ticket());
|
|
396
|
+
}
|
|
397
|
+
|
|
383
398
|
/**
|
|
384
399
|
* Start the LiveSync system
|
|
385
400
|
* Can be called after stop() to restart with a new file
|
|
@@ -417,22 +432,53 @@ class LiveSync {
|
|
|
417
432
|
this._pendingExternal = null;
|
|
418
433
|
this.resumeId = this.generateResumeId();
|
|
419
434
|
|
|
420
|
-
// Seed both merge bases
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
if (this.lane === 'live'
|
|
425
|
-
const clone = captureSnapshot({ flushUndo: false });
|
|
426
|
-
this.lastHtml = serializeForSync(clone);
|
|
427
|
-
this._lastIdentityMap = this.identity.exportMap(clone, originalSnapshotNode);
|
|
428
|
-
this._setDiskBase(captureForSaveAndComparison({ emitForSync: false }).forSave, this._ticket());
|
|
429
|
-
}
|
|
435
|
+
// Seed both merge bases now, so a first frame that arrives after local edits
|
|
436
|
+
// merges instead of holding. A page still booting is refreshed at the settle
|
|
437
|
+
// below: a base taken before a module built its DOM makes the first dirty merge
|
|
438
|
+
// see both sides add that DOM, and keep it twice.
|
|
439
|
+
if (this.lane === 'live') this._seedBases();
|
|
430
440
|
|
|
431
441
|
console.log(`[LiveSync] Starting for: ${this.currentFile} (lane=${this.lane})`);
|
|
432
442
|
// One discovery request stands between here and the stream. It is memoized and
|
|
433
443
|
// bounded, and it does not gate ordinary saves — /_/save is a separate lane that
|
|
434
444
|
// never moved. Snapshots produced during the window are queued, not dropped.
|
|
435
445
|
const gen = ++this._startGen;
|
|
446
|
+
if (this.lane === 'live' && !baselineSettled()) {
|
|
447
|
+
const applyGen = this._applyGen;
|
|
448
|
+
const saveEpoch = this._saveEpoch;
|
|
449
|
+
const startHtml = this.lastHtml;
|
|
450
|
+
const startDiskTicket = this._diskBaseTicket;
|
|
451
|
+
const startDiskSeeded = this._diskBase !== null;
|
|
452
|
+
this._settledHandler = () => {
|
|
453
|
+
this._settledHandler = null;
|
|
454
|
+
if (this.isDestroyed || this._startGen !== gen) return;
|
|
455
|
+
// A person has touched the page: whatever they did (a drag, a button whose
|
|
456
|
+
// handler edits a turn later) may be in it and not yet saved. Keep the start
|
|
457
|
+
// seed, as 1.5.2 did; only an untouched tab gets the settled page as its base.
|
|
458
|
+
if (gestureSeen()) return;
|
|
459
|
+
// The settle clears the gate only when it saw no user edit, so a dirty page
|
|
460
|
+
// here holds work the base must not absorb: keep the start seed.
|
|
461
|
+
if (pageMaybeDirty()) return;
|
|
462
|
+
// A save this tab sent is still unconfirmed: what it carries is not yet
|
|
463
|
+
// common history, and a refused save must merge against the older base.
|
|
464
|
+
if (this._saveTicket !== 0) return;
|
|
465
|
+
// Refresh each lane only while it still holds what start left. A frame, a
|
|
466
|
+
// landed relay, an own save or a disk frame since then gave it a newer base
|
|
467
|
+
// (an own save counts even when its relay failed and lastHtml never moved).
|
|
468
|
+
// Only replace a seed start took, never fill a base start left empty: a page
|
|
469
|
+
// dirty at start held its frames in 1.5.2, and its own scripts may still be
|
|
470
|
+
// rewriting content the merge cannot reach.
|
|
471
|
+
if (startHtml !== null && this._applyGen === applyGen && this._saveEpoch === saveEpoch && this.lastHtml === startHtml) {
|
|
472
|
+
const clone = captureSnapshot({ flushUndo: false });
|
|
473
|
+
this.lastHtml = serializeForSync(clone);
|
|
474
|
+
this._lastIdentityMap = this.identity.exportMap(clone, originalSnapshotNode);
|
|
475
|
+
}
|
|
476
|
+
if (startDiskSeeded && this._diskBaseTicket === startDiskTicket) {
|
|
477
|
+
this._setDiskBase(captureForSaveAndComparison({ emitForSync: false }).forSave, this._ticket());
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
document.addEventListener('clay:baseline-settled', this._settledHandler, { once: true });
|
|
481
|
+
}
|
|
436
482
|
// Exposed so a caller (and the tests) can await the point where the stream is
|
|
437
483
|
// actually open, rather than counting microtasks behind the discovery request.
|
|
438
484
|
this._ready = this._resolveProfile().then(() => {
|
|
@@ -514,6 +560,11 @@ class LiveSync {
|
|
|
514
560
|
this._saveConflictHandler = null;
|
|
515
561
|
}
|
|
516
562
|
|
|
563
|
+
if (this._settledHandler) {
|
|
564
|
+
document.removeEventListener('clay:baseline-settled', this._settledHandler);
|
|
565
|
+
this._settledHandler = null;
|
|
566
|
+
}
|
|
567
|
+
|
|
517
568
|
this._queuedSend = null;
|
|
518
569
|
|
|
519
570
|
// Cancel any pending frame and clear the queue. A morph already in
|