@panphora/clayjs 1.9.2 → 1.11.0
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 +3 -3
- package/dist/clay.standalone.js +9701 -6938
- package/entries/clay-router.js +38 -0
- package/entries/sap.js +6 -6
- package/package.json +2 -2
- package/packed-contract.json +1 -0
- package/src/attrs/add-from.js +72 -0
- package/src/attrs/save-freeze.js +12 -2
- 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/core/snapshot.js +4 -1
- package/src/core/source-map.js +1 -1
- package/src/lib/mutation.js +28 -4
- package/src/lib/person-face.js +16 -0
- package/src/lib/region-capabilities.js +1 -0
- package/src/lib/region-policy.js +10 -3
- package/src/lib/root-attrs.js +28 -0
- 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/plugins/people.js +3 -14
- package/src/plugins/sortable.js +3 -1
- package/src/route/route.js +188 -0
- package/src/standalone.js +1 -0
- package/src/sync/conflict-revert.js +2 -3
- package/src/sync/editing-marker.js +172 -0
- package/src/sync/editing-path.js +46 -0
- package/src/sync/editing-sender.js +100 -0
- package/src/sync/live-sync.js +85 -19
- package/src/sync/presence.js +16 -44
- package/src/sync/stream.js +16 -5
- package/src/vendor/hyper-morph.vendor.js +6 -4
- package/src/sync/section-notice.js +0 -254
package/src/sync/live-sync.js
CHANGED
|
@@ -39,40 +39,60 @@ import { HyperMorph, morph } from "../vendor/hyper-morph.vendor.js";
|
|
|
39
39
|
|
|
40
40
|
const { createIdentityStore } = HyperMorph;
|
|
41
41
|
import Mutation from "../lib/mutation.js";
|
|
42
|
-
import { NO_DIRTY_SELECTOR } from "../lib/region-policy.js";
|
|
42
|
+
import { NO_DIRTY_SELECTOR, LIVE_SELECTOR } from "../lib/region-policy.js";
|
|
43
43
|
import { EXTENSION_NODE_SELECTOR } from '../lib/extension-noise.js';
|
|
44
44
|
import { isEditMode } from "../core/is-edit-mode.js";
|
|
45
45
|
import { mergeTagRecognizers } from "./merge-tags.js";
|
|
46
46
|
import { serializeForSync, captureForComparisonAndDirty, captureSnapshot, originalSnapshotNode, captureForMerge, captureForSaveAndComparison } from '../core/snapshot.js';
|
|
47
|
-
import {
|
|
47
|
+
import { isTabLocalAttr } from '../lib/root-attrs.js';
|
|
48
48
|
import { autosaveActive } from '../lib/autosave-state.js';
|
|
49
49
|
import { enableContentEditable } from '../core/admin-contenteditable.js';
|
|
50
50
|
import { enableOnClick } from '../core/admin-onclick.js';
|
|
51
51
|
import { enableAdminInputs } from '../core/admin-inputs.js';
|
|
52
52
|
import { enableAdminResources } from '../core/admin-resources.js';
|
|
53
53
|
import { presence } from './presence.js';
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
import './
|
|
57
|
-
import { hostMeta } from '../core/host-meta.js';
|
|
54
|
+
import { editingMarker } from './editing-marker.js';
|
|
55
|
+
import { hostMeta, hostSupports } from '../core/host-meta.js';
|
|
56
|
+
import { EditingSender } from './editing-sender.js';
|
|
58
57
|
import { servedDocumentEtag } from '../core/host-attrs.js';
|
|
59
58
|
import { recordEtag, seedEtag, lastSeenEtag, conditionalSaves, representedEtag, forgetRepresentedEtag } from '../core/etag.js';
|
|
60
59
|
import { pageMaybeDirty, pauseGate, resumeGate, gateCaptureToken, gateClearIfUnchanged } from '../lib/dirty-gate.js';
|
|
61
60
|
import { gestureSeen } from '../lib/user-gesture.js';
|
|
62
61
|
import { SyncStream } from './stream.js';
|
|
62
|
+
import { streamOpened, streamClosed, streamEvent, LIVE_EVENTS } from '../live/stream-bridge.js';
|
|
63
63
|
import { conflicts, beginApply, completeApply, failApply } from './conflicts.js';
|
|
64
64
|
import { beginLineageCapture } from './conflict-footprints.js';
|
|
65
65
|
|
|
66
66
|
// What a live-sync merge never reads or touches on any side: editor chrome,
|
|
67
|
-
// content kept out of the save or the snapshot, frozen regions,
|
|
68
|
-
// browser extensions inject. The same set the 0.5 morph's sync policy
|
|
67
|
+
// content kept out of the save or the snapshot, the contents of frozen regions,
|
|
68
|
+
// and nodes browser extensions inject. The same set the 0.5 morph's sync policy
|
|
69
|
+
// ignored.
|
|
69
70
|
const SYNC_IGNORE_SELECTOR = [
|
|
70
71
|
'[editor-ui]', '[clay~="editor-ui"]', '[save-ignore]', '[snapshot-remove]',
|
|
71
|
-
'[no-snapshot]', '[no-save]', '[save-remove]', '[freeze]', '[save-freeze]',
|
|
72
|
-
'[clay~="no-save"]', '[clay~="no-snapshot"]', '[clay~="freeze"]',
|
|
72
|
+
'[no-snapshot]', '[no-save]', '[save-remove]', '[freeze] *', '[save-freeze] *',
|
|
73
|
+
'[clay~="no-save"]', '[clay~="no-snapshot"]', '[clay~="freeze"] *',
|
|
74
|
+
LIVE_SELECTOR,
|
|
73
75
|
EXTENSION_NODE_SELECTOR,
|
|
74
76
|
].join(',');
|
|
75
77
|
|
|
78
|
+
// A frozen element is a scratchpad: the element is part of the document and
|
|
79
|
+
// merges like any other, but its contents belong to this tab. Every merge input
|
|
80
|
+
// has its frozen elements emptied, and hyper-morph's opaque option leaves the
|
|
81
|
+
// live contents alone.
|
|
82
|
+
const FREEZE_SELECTOR = '[clay~="freeze"], [freeze], [save-freeze]';
|
|
83
|
+
|
|
84
|
+
function emptyScratchpads(root) {
|
|
85
|
+
for (const el of root.querySelectorAll(FREEZE_SELECTOR)) el.replaceChildren();
|
|
86
|
+
return root;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function scratchpadsEmptied(input) {
|
|
90
|
+
if (input == null) return input;
|
|
91
|
+
const doc = typeof input === 'string' ? new DOMParser().parseFromString(input, 'text/html') : input;
|
|
92
|
+
emptyScratchpads(doc);
|
|
93
|
+
return doc;
|
|
94
|
+
}
|
|
95
|
+
|
|
76
96
|
// Regions whose local state is never work to keep: disposable content
|
|
77
97
|
// (no-dirty) and regions the mutation system does not watch, whose churn the
|
|
78
98
|
// dirty gate already excludes. The incoming frame's version wins there.
|
|
@@ -81,10 +101,9 @@ const REMOTE_WINS_SELECTOR = [
|
|
|
81
101
|
'[clay~="no-watch"]', '[no-watch]', '[mutations-ignore]',
|
|
82
102
|
].join(',');
|
|
83
103
|
|
|
84
|
-
// Host tokens
|
|
104
|
+
// Host tokens, library state and router state, on every side
|
|
85
105
|
// of a merge: the live root, a snapshot clone's root, a parsed frame's root.
|
|
86
|
-
const isRootLocalAttr = (el, name) =>
|
|
87
|
-
!el.parentElement && el.localName === 'html' && TAB_LOCAL_ROOT_ATTRS.has(name);
|
|
106
|
+
const isRootLocalAttr = (el, name) => isTabLocalAttr(name, el);
|
|
88
107
|
|
|
89
108
|
// Identity an author wrote into the page, the fallback behind synthetic ids.
|
|
90
109
|
//
|
|
@@ -265,6 +284,7 @@ class LiveSync {
|
|
|
265
284
|
// document, and hostMeta() memoizes the request itself.
|
|
266
285
|
this._profile = null;
|
|
267
286
|
this._profilePromise = null;
|
|
287
|
+
this._editing = null;
|
|
268
288
|
this._ready = null;
|
|
269
289
|
// Bumped by every start(), so a discovery that resolves after a stop/restart
|
|
270
290
|
// cannot open a stream for the run that has already been torn down.
|
|
@@ -529,6 +549,12 @@ class LiveSync {
|
|
|
529
549
|
// snapshot listener would never fire — skip registering it.
|
|
530
550
|
if (this.lane === 'live') {
|
|
531
551
|
this.listenForSnapshots();
|
|
552
|
+
hostSupports('presence-editing').then((supported) => {
|
|
553
|
+
if (!supported || this.isDestroyed || this._startGen !== gen || this._editing) return;
|
|
554
|
+
this._editing = new EditingSender({ send: (path) => this._postEditing(path) });
|
|
555
|
+
this._editing.start();
|
|
556
|
+
this._editing.roster(this._lastRoster);
|
|
557
|
+
}, () => {});
|
|
532
558
|
this._saveSavedHandler = () => {
|
|
533
559
|
this._saveEpoch++;
|
|
534
560
|
// The file now holds what this save wrote: the disk lane's new base,
|
|
@@ -572,10 +598,18 @@ class LiveSync {
|
|
|
572
598
|
this.sse.close();
|
|
573
599
|
this.sse = null;
|
|
574
600
|
}
|
|
601
|
+
streamClosed();
|
|
575
602
|
|
|
576
603
|
// Nothing feeds the roster once the stream is gone, so leaving the stack up
|
|
577
604
|
// would show a list of people who may all have left.
|
|
578
605
|
presence.clear();
|
|
606
|
+
editingMarker.clear();
|
|
607
|
+
|
|
608
|
+
if (this._editing) {
|
|
609
|
+
this._editing.stop();
|
|
610
|
+
this._editing = null;
|
|
611
|
+
}
|
|
612
|
+
this._lastRoster = null;
|
|
579
613
|
|
|
580
614
|
if (this._snapshotHandler) {
|
|
581
615
|
document.removeEventListener('clay:snapshot-ready', this._snapshotHandler);
|
|
@@ -760,9 +794,26 @@ class LiveSync {
|
|
|
760
794
|
|
|
761
795
|
this.sse.onopen = () => {
|
|
762
796
|
console.log('[LiveSync] Connected');
|
|
797
|
+
if (this._editing) this._editing.reconnected();
|
|
763
798
|
if (this.onConnect) this.onConnect();
|
|
764
799
|
};
|
|
765
800
|
|
|
801
|
+
// clay.live's fast lane rides this stream: report it open and closed, and pass
|
|
802
|
+
// on its named events (stream-bridge.js).
|
|
803
|
+
this.sse.addEventListener('open', () => streamOpened({ clientId: this.clientId, lane: this.lane }));
|
|
804
|
+
this.sse.addEventListener('error', () => streamClosed());
|
|
805
|
+
for (const type of LIVE_EVENTS) {
|
|
806
|
+
this.sse.addEventListener(type, (event) => {
|
|
807
|
+
let data;
|
|
808
|
+
try {
|
|
809
|
+
data = JSON.parse(event.data);
|
|
810
|
+
} catch {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
streamEvent(type, data);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
766
817
|
// The cursor frame is a NAMED SSE event, so it never reaches onmessage and
|
|
767
818
|
// never looks like data. Its `resync` flag says the server could not retain
|
|
768
819
|
// everything between where this client resumed and the baseline it is
|
|
@@ -819,6 +870,9 @@ class LiveSync {
|
|
|
819
870
|
return;
|
|
820
871
|
}
|
|
821
872
|
presence.update(data);
|
|
873
|
+
editingMarker.update(data);
|
|
874
|
+
this._lastRoster = data;
|
|
875
|
+
if (this._editing) this._editing.roster(data);
|
|
822
876
|
});
|
|
823
877
|
|
|
824
878
|
this.sse.onmessage = (event) => {
|
|
@@ -1083,6 +1137,16 @@ class LiveSync {
|
|
|
1083
1137
|
});
|
|
1084
1138
|
}
|
|
1085
1139
|
|
|
1140
|
+
_postEditing(path) {
|
|
1141
|
+
const url = new URL('/_/presence', window.location.origin);
|
|
1142
|
+
url.searchParams.set('client-id', this.clientId);
|
|
1143
|
+
return fetch(url.href, {
|
|
1144
|
+
method: 'POST',
|
|
1145
|
+
headers: { 'Content-Type': 'application/json', 'Document-URL': window.location.href },
|
|
1146
|
+
body: JSON.stringify({ editing: path }),
|
|
1147
|
+
}).then((response) => response.ok, () => false);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1086
1150
|
/**
|
|
1087
1151
|
* Route an external disk change (htmlclay watcher) out of the notification
|
|
1088
1152
|
* path. Returns true when the notification was consumed — the caller must
|
|
@@ -1592,7 +1656,7 @@ class LiveSync {
|
|
|
1592
1656
|
// A frame may neither write these onto our root nor, by not carrying them,
|
|
1593
1657
|
// take ours away. Returning false is hyper-morph's veto for both directions.
|
|
1594
1658
|
const beforeAttributeUpdated = (name, element) =>
|
|
1595
|
-
|
|
1659
|
+
isTabLocalAttr(name, element) ? false : undefined;
|
|
1596
1660
|
const authored = uniqueAuthoredIdentity(new WeakMap());
|
|
1597
1661
|
const sideIdentity = (map) =>
|
|
1598
1662
|
isIdMap(map) ? { first: authored, map, then: authored } : authored;
|
|
@@ -1619,11 +1683,13 @@ class LiveSync {
|
|
|
1619
1683
|
if (synthetic) syntheticByAuthored.set(id, synthetic);
|
|
1620
1684
|
}
|
|
1621
1685
|
}
|
|
1686
|
+
remote = scratchpadsEmptied(remote);
|
|
1622
1687
|
|
|
1623
1688
|
const common = {
|
|
1624
1689
|
live: document,
|
|
1625
1690
|
remote,
|
|
1626
1691
|
ignore: (el) => el.matches(SYNC_IGNORE_SELECTOR),
|
|
1692
|
+
opaque: (el) => el.matches(FREEZE_SELECTOR),
|
|
1627
1693
|
remoteWins: (el) => el.matches(REMOTE_WINS_SELECTOR),
|
|
1628
1694
|
ignoreAttribute: isRootLocalAttr,
|
|
1629
1695
|
protectFocusedValue: true,
|
|
@@ -1648,7 +1714,7 @@ class LiveSync {
|
|
|
1648
1714
|
pending = mergeDocument({
|
|
1649
1715
|
...common,
|
|
1650
1716
|
base: null,
|
|
1651
|
-
local: { root: captureLocal(), toLive: originalSnapshotNode },
|
|
1717
|
+
local: { root: emptyScratchpads(captureLocal()), toLive: originalSnapshotNode },
|
|
1652
1718
|
identity: { local: localIdentity, remote: sideIdentity(identityMap) },
|
|
1653
1719
|
});
|
|
1654
1720
|
} else if (!dirty) {
|
|
@@ -1660,7 +1726,7 @@ class LiveSync {
|
|
|
1660
1726
|
// a local edit, and a clean autosave tab would write that "edit" back to
|
|
1661
1727
|
// disk on every frame. Still three-way: merge tags keep their base for
|
|
1662
1728
|
// the JSON merge, and hyper-morph's clean-side shortcuts apply.
|
|
1663
|
-
const localRoot = captureLocal();
|
|
1729
|
+
const localRoot = emptyScratchpads(captureLocal());
|
|
1664
1730
|
const baseDoc = localRoot.ownerDocument;
|
|
1665
1731
|
if (baseDoc.documentElement !== localRoot) baseDoc.replaceChild(localRoot, baseDoc.documentElement);
|
|
1666
1732
|
pending = mergeDocument({
|
|
@@ -1675,7 +1741,7 @@ class LiveSync {
|
|
|
1675
1741
|
},
|
|
1676
1742
|
});
|
|
1677
1743
|
} else {
|
|
1678
|
-
localRoot = captureLocal();
|
|
1744
|
+
localRoot = emptyScratchpads(captureLocal());
|
|
1679
1745
|
applyId = beginApply({
|
|
1680
1746
|
source, seq, etag, root: localRoot,
|
|
1681
1747
|
domain: source === 'disk' ? 'save' : 'sync',
|
|
@@ -1683,7 +1749,7 @@ class LiveSync {
|
|
|
1683
1749
|
try {
|
|
1684
1750
|
pending = mergeDocument({
|
|
1685
1751
|
...common,
|
|
1686
|
-
base,
|
|
1752
|
+
base: scratchpadsEmptied(base),
|
|
1687
1753
|
local: { root: localRoot, toLive: originalSnapshotNode },
|
|
1688
1754
|
identity: {
|
|
1689
1755
|
base: sideIdentity(baseIdentityMap),
|
|
@@ -1920,7 +1986,7 @@ class LiveSync {
|
|
|
1920
1986
|
// returns above without reaching here, so nothing can name an author for a
|
|
1921
1987
|
// change this tab never took. Null on every frame the host did not stamp.
|
|
1922
1988
|
document.dispatchEvent(new CustomEvent('clay:sync-applied', {
|
|
1923
|
-
detail: { seq, source: 'peer', by: by || null, report, unresolved: this.unresolvedConflicts, conflictIds }
|
|
1989
|
+
detail: { seq, source: 'peer', by: by || null, html, report, unresolved: this.unresolvedConflicts, conflictIds }
|
|
1924
1990
|
}));
|
|
1925
1991
|
} finally {
|
|
1926
1992
|
this._log('applyUpdate - morph complete, resuming mutations');
|
package/src/sync/presence.js
CHANGED
|
@@ -31,12 +31,14 @@
|
|
|
31
31
|
* hyperclay-local, HTML Clay and makerclay do today and it stays that way until
|
|
32
32
|
* they adopt it.
|
|
33
33
|
*
|
|
34
|
-
* `people[].id` is
|
|
35
|
-
*
|
|
34
|
+
* `people[].id` is the person's people id (the one `clay.me` and `clay.people`
|
|
35
|
+
* use) or a guest's per-document pseudonym. It picks the colour and nothing
|
|
36
|
+
* else: it is not a permission check, and it is never logged.
|
|
36
37
|
*/
|
|
37
38
|
|
|
38
39
|
import { hostSupports } from '../core/host-meta.js';
|
|
39
40
|
import { make, set } from '../lib/hostile-css.js';
|
|
41
|
+
import { initialsOf, personColor } from '../lib/person-face.js';
|
|
40
42
|
import { pageScheme, RUNTIME_ONLY } from '../ui/bevel-controls.js';
|
|
41
43
|
import { TOKENS, FONT_MONO, SHADOW } from '../ui/bevel.js';
|
|
42
44
|
|
|
@@ -51,47 +53,17 @@ const LABEL_FONT = `500 11px/1.4 ${FONT_MONO}`;
|
|
|
51
53
|
// resets color-scheme and direction too, which are put back straight after.
|
|
52
54
|
const RESET = ['all:initial', 'color-scheme:inherit', 'direction:ltr', 'unicode-bidi:isolate'];
|
|
53
55
|
|
|
54
|
-
// The
|
|
55
|
-
//
|
|
56
|
-
// against it.
|
|
56
|
+
// The fill of every square, under the participant's colour. One literal in both
|
|
57
|
+
// schemes, because every colour the shared person-face palette draws with was
|
|
58
|
+
// chosen against it.
|
|
57
59
|
const FACE = '#FFFCF6';
|
|
58
60
|
|
|
59
|
-
// A fixed set rather than a computed hue: every one of these was looked at
|
|
60
|
-
// against white initials, and a hue wheel puts neighbouring pseudonyms on two
|
|
61
|
-
// blues nobody can tell apart.
|
|
62
|
-
const COLORS = [
|
|
63
|
-
'#b91c1c', '#c2410c', '#a16207', '#15803d', '#0f766e',
|
|
64
|
-
'#0369a1', '#1d4ed8', '#6d28d9', '#a21caf', '#9f1239',
|
|
65
|
-
];
|
|
66
|
-
|
|
67
61
|
/** Every element this module creates, marked the same way. */
|
|
68
62
|
function runtimeRoot(el) {
|
|
69
63
|
el.setAttribute('clay', RUNTIME_ONLY);
|
|
70
64
|
return el;
|
|
71
65
|
}
|
|
72
66
|
|
|
73
|
-
function hashOf(id) {
|
|
74
|
-
let h = 5381;
|
|
75
|
-
for (let i = 0; i < id.length; i++) {
|
|
76
|
-
h = (((h << 5) + h) ^ id.charCodeAt(i)) >>> 0;
|
|
77
|
-
}
|
|
78
|
-
return h;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// The pseudonym picks the colour and nothing else.
|
|
82
|
-
function colorFor(id) {
|
|
83
|
-
return COLORS[hashOf(id) % COLORS.length];
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Array.from, not [0]: a name starting outside the BMP would otherwise show half
|
|
87
|
-
// a character.
|
|
88
|
-
function initialsOf(name) {
|
|
89
|
-
const words = String(name).trim().split(/\s+/).filter(Boolean);
|
|
90
|
-
if (!words.length) return '?';
|
|
91
|
-
const first = (word) => Array.from(word)[0].toUpperCase();
|
|
92
|
-
return words.length === 1 ? first(words[0]) : first(words[0]) + first(words[1]);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
67
|
/**
|
|
96
68
|
* Read the roster frame defensively. Everything here arrived over a wire, and a
|
|
97
69
|
* frame this function cannot understand must draw nothing rather than draw a
|
|
@@ -106,7 +78,6 @@ function normalize(data) {
|
|
|
106
78
|
people.push({
|
|
107
79
|
id: person.id,
|
|
108
80
|
name,
|
|
109
|
-
canEdit: person.canEdit === true,
|
|
110
81
|
you: person.you === true,
|
|
111
82
|
});
|
|
112
83
|
}
|
|
@@ -144,8 +115,14 @@ class Presence {
|
|
|
144
115
|
* @returns {Promise<void>}
|
|
145
116
|
*/
|
|
146
117
|
async update(data) {
|
|
147
|
-
|
|
118
|
+
const next = normalize(data);
|
|
119
|
+
const same = this.root && JSON.stringify(next) === JSON.stringify(this.roster);
|
|
120
|
+
this.roster = next;
|
|
148
121
|
if (!(await this.supported())) return;
|
|
122
|
+
if (same) {
|
|
123
|
+
set(this.root, 'color-scheme', pageScheme());
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
149
126
|
this.render();
|
|
150
127
|
}
|
|
151
128
|
|
|
@@ -260,13 +237,8 @@ class Presence {
|
|
|
260
237
|
}
|
|
261
238
|
|
|
262
239
|
avatar(person) {
|
|
263
|
-
const color =
|
|
264
|
-
|
|
265
|
-
// reading it. The two are the same three colours inverted, so a stack of
|
|
266
|
-
// both reads as one set rather than as two kinds of thing.
|
|
267
|
-
const skin = person.canEdit
|
|
268
|
-
? [`background-color:${color}`, `color:${FACE}`, `border-color:${FACE}`]
|
|
269
|
-
: [`background-color:${FACE}`, `color:${color}`, `border-color:${color}`];
|
|
240
|
+
const color = personColor(person.id);
|
|
241
|
+
const skin = [`background-color:${FACE}`, `color:${color}`, `border-color:${color}`];
|
|
270
242
|
|
|
271
243
|
const el = runtimeRoot(make('div', [
|
|
272
244
|
...RESET,
|
package/src/sync/stream.js
CHANGED
|
@@ -2,6 +2,12 @@ const START_TIMEOUT = 5000;
|
|
|
2
2
|
const HIDDEN_DELAY = 5000;
|
|
3
3
|
const PING_INTERVAL = 15000;
|
|
4
4
|
|
|
5
|
+
let workspaceBackground = window.__clayWorkspaceBackground === true;
|
|
6
|
+
document.addEventListener('clay:workspace-background', () => { workspaceBackground = true; });
|
|
7
|
+
document.addEventListener('clay:workspace-foreground', () => { workspaceBackground = false; });
|
|
8
|
+
|
|
9
|
+
const pageHidden = () => document.hidden || workspaceBackground;
|
|
10
|
+
|
|
5
11
|
export class SyncStream extends EventTarget {
|
|
6
12
|
constructor(url, { shared = false, documentURL, lane, startupCheck = false } = {}) {
|
|
7
13
|
super();
|
|
@@ -28,7 +34,7 @@ export class SyncStream extends EventTarget {
|
|
|
28
34
|
this._source = null;
|
|
29
35
|
this._visibility = () => {
|
|
30
36
|
clearTimeout(this._hiddenTimer);
|
|
31
|
-
if (
|
|
37
|
+
if (pageHidden()) {
|
|
32
38
|
this._worker?.port.postMessage({ v: 1, type: 'hidden' });
|
|
33
39
|
this._hiddenTimer = setTimeout(() => this._suspend(), HIDDEN_DELAY);
|
|
34
40
|
} else {
|
|
@@ -39,12 +45,14 @@ export class SyncStream extends EventTarget {
|
|
|
39
45
|
}
|
|
40
46
|
};
|
|
41
47
|
this._pagehide = () => this._suspend();
|
|
42
|
-
this._pageshow = () => { if (!
|
|
48
|
+
this._pageshow = () => { if (!pageHidden()) this._resume(); };
|
|
43
49
|
document.addEventListener('visibilitychange', this._visibility);
|
|
50
|
+
document.addEventListener('clay:workspace-background', this._visibility);
|
|
51
|
+
document.addEventListener('clay:workspace-foreground', this._visibility);
|
|
44
52
|
window.addEventListener('pagehide', this._pagehide);
|
|
45
53
|
window.addEventListener('pageshow', this._pageshow);
|
|
46
54
|
this._open();
|
|
47
|
-
if (
|
|
55
|
+
if (pageHidden()) this._visibility();
|
|
48
56
|
}
|
|
49
57
|
|
|
50
58
|
_emit(type, data) {
|
|
@@ -99,7 +107,7 @@ export class SyncStream extends EventTarget {
|
|
|
99
107
|
}
|
|
100
108
|
};
|
|
101
109
|
worker.port.start();
|
|
102
|
-
worker.port.postMessage({ v: 1, type:
|
|
110
|
+
worker.port.postMessage({ v: 1, type: pageHidden() ? 'hidden' : 'visible' });
|
|
103
111
|
worker.port.postMessage({ v: 1, type: 'subscribe', document: this._documentURL, lane: this._lane, since: this._since });
|
|
104
112
|
this._pingTimer = setInterval(() => {
|
|
105
113
|
if (this._worker !== worker) return;
|
|
@@ -139,7 +147,8 @@ export class SyncStream extends EventTarget {
|
|
|
139
147
|
this._remember(event.data);
|
|
140
148
|
this._emit('message', event.data);
|
|
141
149
|
};
|
|
142
|
-
|
|
150
|
+
// `data`, `data-state` and `data-writer` are clay.live's fast lane (stream-bridge.js).
|
|
151
|
+
for (const type of ['cursor', 'presence', 'data', 'data-state', 'data-writer']) {
|
|
143
152
|
source.addEventListener(type, event => {
|
|
144
153
|
if (this._source === source) this._emit(type, event.data);
|
|
145
154
|
});
|
|
@@ -197,6 +206,8 @@ export class SyncStream extends EventTarget {
|
|
|
197
206
|
clearTimeout(this._hiddenTimer);
|
|
198
207
|
this._disconnect();
|
|
199
208
|
document.removeEventListener('visibilitychange', this._visibility);
|
|
209
|
+
document.removeEventListener('clay:workspace-background', this._visibility);
|
|
210
|
+
document.removeEventListener('clay:workspace-foreground', this._visibility);
|
|
200
211
|
window.removeEventListener('pagehide', this._pagehide);
|
|
201
212
|
window.removeEventListener('pageshow', this._pageshow);
|
|
202
213
|
}
|