@panphora/clayjs 1.10.0 → 1.11.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 +2 -2
- package/dist/clay.standalone.js +7104 -4924
- package/entries/sap.js +6 -6
- package/package.json +1 -1
- package/src/attrs/add-from.js +72 -0
- package/src/core/conflict-notice.js +2 -5
- package/src/core/conflict-presentation.js +42 -0
- package/src/core/save-core.js +22 -0
- package/src/core/save.js +11 -2
- package/src/lib/region-capabilities.js +1 -0
- package/src/lib/region-policy.js +10 -3
- package/src/live/checkpoint.js +106 -0
- package/src/live/fast-lane.js +590 -0
- package/src/live/local-lane.js +582 -0
- package/src/live/ops.js +291 -0
- package/src/live/server-lane.js +84 -0
- package/src/live/store.js +291 -0
- package/src/live/stream-bridge.js +60 -0
- package/src/loader-logic.js +7 -2
- package/src/loader.js +2 -0
- package/src/plugins/live.js +446 -0
- package/src/sync/conflict-revert.js +1 -1
- package/src/sync/live-sync.js +21 -5
- package/src/sync/presence.js +4 -10
- package/src/sync/stream.js +2 -1
- package/src/vendor/hyper-morph.vendor.js +6 -4
- package/src/sync/section-notice.js +0 -254
package/src/lib/region-policy.js
CHANGED
|
@@ -49,7 +49,7 @@ import { capabilitySelector, expandsTo, hasCapability, hasPolicyToken, POLICY_TO
|
|
|
49
49
|
export const PERSIST = { FULL: 'full', FROZEN: 'frozen', NONE: 'none' };
|
|
50
50
|
|
|
51
51
|
// The canonical region tokens (spelled in the `clay` attribute or as bare attrs).
|
|
52
|
-
export const REGION_ATTRS = ['no-save', 'no-trigger-autosave', 'no-dirty', 'no-undo', 'no-watch', 'no-data', 'freeze', 'editor-ui'];
|
|
52
|
+
export const REGION_ATTRS = ['no-save', 'no-trigger-autosave', 'no-dirty', 'no-undo', 'no-watch', 'no-data', 'freeze', 'editor-ui', 'live'];
|
|
53
53
|
|
|
54
54
|
// Every canonical token spellable inside the space-separated `clay` attribute.
|
|
55
55
|
// Exported: this list used to exist as a private CLAY_TOKENS that nothing read,
|
|
@@ -83,7 +83,7 @@ export const FREEZE_SELECTOR = '[clay~="freeze"], [freeze], [save-freeze]';
|
|
|
83
83
|
// mutations-ignore footgun (their content stays in the saved file, but is no
|
|
84
84
|
// longer counted as a change).
|
|
85
85
|
export const STRIP_FROM_COMPARISON =
|
|
86
|
-
'[clay~="no-save"], [clay~="no-trigger-autosave"], [clay~="no-dirty"], [clay~="freeze"], [clay~="no-watch"], [no-save], [save-remove], [no-trigger-autosave], [no-dirty], [save-ignore], [freeze], [save-freeze], [no-watch], [mutations-ignore]';
|
|
86
|
+
'[clay~="no-save"], [clay~="no-trigger-autosave"], [clay~="no-dirty"], [clay~="freeze"], [clay~="no-watch"], [no-save], [save-remove], [no-trigger-autosave], [no-dirty], [save-ignore], [freeze], [save-freeze], [no-watch], [mutations-ignore], [clay~="live"]';
|
|
87
87
|
|
|
88
88
|
// Every spelling of no-trigger-autosave: the ONE difference between the autosave
|
|
89
89
|
// domain and the dirty domain. A document containing none of these has identical
|
|
@@ -94,6 +94,10 @@ export const STRIP_FROM_COMPARISON =
|
|
|
94
94
|
export const NO_TRIGGER_AUTOSAVE_SELECTOR =
|
|
95
95
|
'[clay~="no-trigger-autosave"], [no-trigger-autosave]';
|
|
96
96
|
|
|
97
|
+
// Live JSON stores (clay="live" on a merge= JSON script). Saved, stripped from both
|
|
98
|
+
// comparison domains, and ignored by the live-sync merge: the live plugin owns the value.
|
|
99
|
+
export const LIVE_SELECTOR = '[clay~="live"]';
|
|
100
|
+
|
|
97
101
|
// Disposable regions: saved, but their content is explicitly not work. Stripped
|
|
98
102
|
// from BOTH comparison domains, skipped by the merge on both sides, and never
|
|
99
103
|
// counted by the close warning. This is the declared signal that lets the merge
|
|
@@ -108,7 +112,7 @@ export const NO_DIRTY_SELECTOR =
|
|
|
108
112
|
// dirty-tracked either, and counting it would re-freeze the live-sync baseline
|
|
109
113
|
// the way the mounted-tool bug did (see dirty-gate.js).
|
|
110
114
|
export const STRIP_FROM_DIRTY_CHECK =
|
|
111
|
-
'[clay~="no-save"], [clay~="no-dirty"], [clay~="freeze"], [clay~="no-watch"], [no-save], [save-remove], [no-dirty], [save-ignore], [freeze], [save-freeze], [no-watch], [mutations-ignore]';
|
|
115
|
+
'[clay~="no-save"], [clay~="no-dirty"], [clay~="freeze"], [clay~="no-watch"], [no-save], [save-remove], [no-dirty], [save-ignore], [freeze], [save-freeze], [no-watch], [mutations-ignore], [clay~="live"]';
|
|
112
116
|
|
|
113
117
|
// Snapshot-layer marker: removed from EVERY snapshot (save, live-sync broadcast,
|
|
114
118
|
// dirty-comparison) in snapshot.js. `no-snapshot` is the consistent alias for the
|
|
@@ -163,6 +167,9 @@ export function resolveRegionPolicy(node) {
|
|
|
163
167
|
if (expandsTo(element, 'no-watch')) watched = false;
|
|
164
168
|
if (hasRegionToken(element, 'no-trigger-autosave')) autosaveOff = true;
|
|
165
169
|
if (hasRegionToken(element, 'no-dirty')) { autosaveOff = true; dirtyOff = true; }
|
|
170
|
+
// A live JSON store: saved, but its value lives in memory and the live plugin
|
|
171
|
+
// writes it into each save clone, so the tag itself is never work to track.
|
|
172
|
+
if (hasRegionToken(element, 'live')) { autosaveOff = true; dirtyOff = true; undoable = false; }
|
|
166
173
|
if (expandsTo(element, 'no-undo')) undoable = false;
|
|
167
174
|
if (expandsTo(element, 'no-save')) persistRank = Math.max(persistRank, PERSIST_RANK.none);
|
|
168
175
|
if (hasRegionToken(element, 'freeze')) persistRank = Math.max(persistRank, PERSIST_RANK.frozen);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Checkpoints for clay.live stores. A live change never marks the page unsaved
|
|
2
|
+
// (live tags are outside the comparison domains), so nothing else would ever save
|
|
3
|
+
// it. This does: at a fixed deadline CHECKPOINT_MS after the first unsaved change,
|
|
4
|
+
// so a steady stream of moves still saves on time, and once more, right away,
|
|
5
|
+
// when the tab is hidden or closed.
|
|
6
|
+
|
|
7
|
+
export const CHECKPOINT_MS = 8000;
|
|
8
|
+
export const RETRY_MS = 1000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {object} deps
|
|
12
|
+
* @param {() => boolean} deps.needsSave - is there a live change no accepted save holds
|
|
13
|
+
* @param {() => boolean} deps.blocked - must a save wait (settling, conflict, agent editing)
|
|
14
|
+
* @param {() => Promise<object>} deps.save - the checkpoint save; resolves { ok, msgType }
|
|
15
|
+
* @param {() => unknown} deps.saveNow - the hide/close save, sent with keepalive
|
|
16
|
+
*/
|
|
17
|
+
export function createCheckpoints({ needsSave, blocked, save, saveNow, now = () => Date.now(), setTimer = setTimeout, clearTimer = clearTimeout }) {
|
|
18
|
+
let deadline = null;
|
|
19
|
+
let timer = null;
|
|
20
|
+
let running = false;
|
|
21
|
+
let flushing = false;
|
|
22
|
+
// After a save that did not land, no save before this time (soon included).
|
|
23
|
+
let retryAt = 0;
|
|
24
|
+
|
|
25
|
+
function after(result) {
|
|
26
|
+
const skipped = result && !result.ok && result.msgType === "skipped";
|
|
27
|
+
retryAt = result?.ok || skipped ? 0 : now() + CHECKPOINT_MS;
|
|
28
|
+
return skipped ? RETRY_MS : CHECKPOINT_MS;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function arm(at) {
|
|
32
|
+
deadline = at;
|
|
33
|
+
if (timer !== null) clearTimer(timer);
|
|
34
|
+
timer = setTimer(run, Math.max(0, at - now()));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function disarm() {
|
|
38
|
+
if (timer !== null) clearTimer(timer);
|
|
39
|
+
timer = null;
|
|
40
|
+
deadline = null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function run() {
|
|
44
|
+
timer = null;
|
|
45
|
+
deadline = null;
|
|
46
|
+
if (!needsSave()) return;
|
|
47
|
+
if (blocked()) {
|
|
48
|
+
arm(now() + RETRY_MS);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
running = true;
|
|
52
|
+
let result = null;
|
|
53
|
+
try {
|
|
54
|
+
result = await save();
|
|
55
|
+
} catch {
|
|
56
|
+
result = null;
|
|
57
|
+
} finally {
|
|
58
|
+
running = false;
|
|
59
|
+
}
|
|
60
|
+
const wait = after(result);
|
|
61
|
+
if (!needsSave()) return;
|
|
62
|
+
// A save skipped because another was on the wire is retried soon; anything
|
|
63
|
+
// else waits a full interval, so a failing host is not asked every second.
|
|
64
|
+
arm(now() + wait);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
// A live change happened. The first one after a save starts the clock; later
|
|
69
|
+
// ones do not move it.
|
|
70
|
+
changed() {
|
|
71
|
+
if (deadline === null && !running) arm(now() + CHECKPOINT_MS);
|
|
72
|
+
},
|
|
73
|
+
// The tab is going away or out of sight: save now if anything is unsaved.
|
|
74
|
+
// A save that did not land arms the clock again, as run() does.
|
|
75
|
+
flush() {
|
|
76
|
+
if (running || !needsSave() || blocked()) return;
|
|
77
|
+
disarm();
|
|
78
|
+
running = true;
|
|
79
|
+
flushing = true;
|
|
80
|
+
Promise.resolve()
|
|
81
|
+
.then(saveNow)
|
|
82
|
+
.catch(() => null)
|
|
83
|
+
.then((result) => {
|
|
84
|
+
running = false;
|
|
85
|
+
flushing = false;
|
|
86
|
+
const wait = after(result);
|
|
87
|
+
if (!needsSave()) return;
|
|
88
|
+
arm(now() + wait);
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
// Save as soon as possible: the room asks for it (its log is half full, or
|
|
92
|
+
// the file holds a version the room cannot rebuild from).
|
|
93
|
+
soon() {
|
|
94
|
+
if (running) return;
|
|
95
|
+
const at = Math.max(now(), retryAt);
|
|
96
|
+
if (deadline === null || at < deadline) arm(at);
|
|
97
|
+
},
|
|
98
|
+
// True while the hide/close save is being made.
|
|
99
|
+
get flushing() {
|
|
100
|
+
return flushing;
|
|
101
|
+
},
|
|
102
|
+
get deadline() {
|
|
103
|
+
return deadline;
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|