@panphora/clayjs 1.12.1 → 1.13.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 +1 -1
- package/dist/clay.standalone.js +3544 -3167
- package/docs/guide.md +1847 -0
- package/entries/clay-data.js +1 -1
- package/entries/sap.js +6 -6
- package/package.json +14 -1
- package/packed-contract.json +1 -1
- package/src/lib/unsaved-state.js +11 -2
- package/src/live/checkpoint.js +7 -0
- package/src/live/fast-lane.js +24 -10
- package/src/live/local-lane.js +14 -7
- package/src/live/moves.js +11 -0
- package/src/live/server-lane.js +2 -1
- package/src/live/store.js +32 -1
- package/src/loader-logic.js +5 -1
- package/src/loader.js +2 -0
- package/src/page/binding.js +647 -0
- package/src/page/inline.js +113 -0
- package/src/page/model.js +241 -0
- package/src/page/replay.js +578 -0
- package/src/page/schema.js +157 -0
- package/src/plugins/live.js +49 -2
- package/src/plugins/message.js +159 -0
- package/src/plugins/visitor-drafts/prompt-style.js +5 -16
- package/src/plugins/visitor-drafts/prompt.js +6 -41
- package/src/plugins/visitor-drafts/source.js +0 -5
- package/src/plugins/visitor-drafts.js +2 -2
- package/src/vendor/hyper-html-api-data.vendor.js +1 -1
- package/src/vendor/hypercms.vendor.js +20 -20
- package/src/vendor/prosemirror.vendor.js +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@panphora/clayjs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "clayjs: malleable documents. Save lifecycle for self-saving HTML.",
|
|
6
6
|
"license": "MIT-0",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"exports": {
|
|
30
30
|
"./package.json": "./package.json",
|
|
31
31
|
"./THIRD-PARTY-NOTICES.md": "./THIRD-PARTY-NOTICES.md",
|
|
32
|
+
"./guide.md": "./docs/guide.md",
|
|
32
33
|
"./src/*": "./src/*",
|
|
33
34
|
"./entries/*": "./entries/*",
|
|
34
35
|
"./clay.standalone.js": "./dist/clay.standalone.js",
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"entries/",
|
|
42
43
|
"dist/",
|
|
43
44
|
"THIRD-PARTY-NOTICES.md",
|
|
45
|
+
"docs/guide.md",
|
|
44
46
|
"src/"
|
|
45
47
|
],
|
|
46
48
|
"publishConfig": {
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
"check:foundation": "node scripts/propagate-foundation.mjs --check",
|
|
61
63
|
"deploy": "node build.js && wrangler deploy && git rev-parse HEAD > .deploy",
|
|
62
64
|
"build:parse5": "node scripts/build-parse5-vendor.mjs",
|
|
65
|
+
"build:prosemirror": "node scripts/build-prosemirror-vendor.mjs",
|
|
63
66
|
"build:bevel": "node scripts/build-bevel-subset.mjs",
|
|
64
67
|
"check:bevel": "node scripts/build-bevel-subset.mjs --check",
|
|
65
68
|
"build:skins": "node scripts/build-vendor-skins.mjs",
|
|
@@ -79,6 +82,16 @@
|
|
|
79
82
|
"parse5": "^7.3.0",
|
|
80
83
|
"playwright": "1.62.1",
|
|
81
84
|
"postcss": "^8.5.28",
|
|
85
|
+
"prosemirror-commands": "1.7.2",
|
|
86
|
+
"prosemirror-dropcursor": "1.8.4",
|
|
87
|
+
"prosemirror-gapcursor": "1.4.1",
|
|
88
|
+
"prosemirror-history": "1.5.1",
|
|
89
|
+
"prosemirror-inputrules": "1.5.1",
|
|
90
|
+
"prosemirror-keymap": "1.2.3",
|
|
91
|
+
"prosemirror-model": "1.25.12",
|
|
92
|
+
"prosemirror-state": "1.4.4",
|
|
93
|
+
"prosemirror-transform": "1.12.2",
|
|
94
|
+
"prosemirror-view": "1.42.6",
|
|
82
95
|
"turndown": "^7.2.4",
|
|
83
96
|
"turndown-plugin-gfm": "^1.0.2"
|
|
84
97
|
},
|
package/packed-contract.json
CHANGED
package/src/lib/unsaved-state.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
const sources = new Map();
|
|
12
12
|
let nextKey = 0;
|
|
13
13
|
|
|
14
|
-
export function registerUnsavedState({ id = '', isPending }) {
|
|
14
|
+
export function registerUnsavedState({ id = '', isPending, flush = null }) {
|
|
15
15
|
const key = ++nextKey;
|
|
16
|
-
sources.set(key, { id, isPending });
|
|
16
|
+
sources.set(key, { id, isPending, flush });
|
|
17
17
|
let disposed = false;
|
|
18
18
|
return {
|
|
19
19
|
changed() {
|
|
@@ -49,6 +49,15 @@ export function hasUnsavedState() {
|
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Ask every pending source that can finish on its own (a checkpoint, a queued
|
|
53
|
+
// upload) to do so, and wait. Sources without a flush (an open composition)
|
|
54
|
+
// stay pending; they are listed in the answer, never waited on.
|
|
55
|
+
export async function flushUnsaved() {
|
|
56
|
+
const flushes = [...sources.values()].filter((s) => typeof s.flush === 'function' && isPending(s));
|
|
57
|
+
const results = await Promise.all(flushes.map((s) => Promise.resolve().then(s.flush).catch(() => ({ ok: false }))));
|
|
58
|
+
return { ok: results.every((r) => r?.ok !== false), pending: pendingIds() };
|
|
59
|
+
}
|
|
60
|
+
|
|
52
61
|
if (typeof window !== 'undefined') {
|
|
53
62
|
window.clay = window.clay || {};
|
|
54
63
|
window.clay.registerUnsavedState = registerUnsavedState;
|
package/src/live/checkpoint.js
CHANGED
|
@@ -95,6 +95,13 @@ export function createCheckpoints({ needsSave, blocked, save, saveNow, now = ()
|
|
|
95
95
|
const at = Math.max(now(), retryAt);
|
|
96
96
|
if (deadline === null || at < deadline) arm(at);
|
|
97
97
|
},
|
|
98
|
+
// Save no later than ms from now (the page editor's pause save). Never
|
|
99
|
+
// later than a deadline already set, never before a failed save's retry time.
|
|
100
|
+
within(ms) {
|
|
101
|
+
if (running) return;
|
|
102
|
+
const at = Math.max(now() + ms, retryAt);
|
|
103
|
+
if (deadline === null || at < deadline) arm(at);
|
|
104
|
+
},
|
|
98
105
|
// True while the hide/close save is being made.
|
|
99
106
|
get flushing() {
|
|
100
107
|
return flushing;
|
package/src/live/fast-lane.js
CHANGED
|
@@ -91,6 +91,8 @@ export function createFastLane({
|
|
|
91
91
|
// newest op id ever put in a frame.
|
|
92
92
|
const lanes = new Map();
|
|
93
93
|
|
|
94
|
+
const tell = (name, event) => entries.get(name)?.store.deliver?.({ code: null, n: null, seq: null, through: null, retryAfter: null, ...event });
|
|
95
|
+
|
|
94
96
|
function laneOf(name) {
|
|
95
97
|
let lane = lanes.get(name);
|
|
96
98
|
if (!lane) lanes.set(name, (lane = { n: 0, unsent: [], awaiting: [], framed: 0 }));
|
|
@@ -115,7 +117,7 @@ export function createFastLane({
|
|
|
115
117
|
const store = entries.get(name)?.store;
|
|
116
118
|
let through = 0;
|
|
117
119
|
for (const frame of lane.awaiting) if (!unapplied(frame)) through = frame.through;
|
|
118
|
-
if (store && through) store.rebase(store.authoritative, { source: "state", through });
|
|
120
|
+
if (store && through) store.rebase(store.authoritative, { source: "state", through, cause: "leave" });
|
|
119
121
|
onLeave(name, { epoch, seq });
|
|
120
122
|
lane.unsent = [];
|
|
121
123
|
lane.awaiting = [];
|
|
@@ -156,7 +158,8 @@ export function createFastLane({
|
|
|
156
158
|
const [entry] = lane.unsent.splice(0, 1);
|
|
157
159
|
console.warn(`clay.live("${name}"): one update was larger than ${FRAME_BYTES} bytes, so it was undone`);
|
|
158
160
|
const store = entries.get(name)?.store;
|
|
159
|
-
store?.rebase(store.authoritative, { source: "state", through: entry.id });
|
|
161
|
+
store?.rebase(store.authoritative, { source: "state", through: entry.id, cause: "undone" });
|
|
162
|
+
tell(name, { phase: "undone", code: "op-too-large", through: entry.id });
|
|
160
163
|
continue;
|
|
161
164
|
}
|
|
162
165
|
const take = lane.unsent.splice(0, count);
|
|
@@ -178,6 +181,7 @@ export function createFastLane({
|
|
|
178
181
|
const sentEpoch = epoch;
|
|
179
182
|
frame.sentOn = connection;
|
|
180
183
|
frame.sent = true;
|
|
184
|
+
tell(name, { phase: "sent", n: frame.n, through: frame.through });
|
|
181
185
|
// Refusal is evidence about earlier attempts only: this one may apply.
|
|
182
186
|
frame.refused = false;
|
|
183
187
|
inFlight = true;
|
|
@@ -212,7 +216,7 @@ export function createFastLane({
|
|
|
212
216
|
}
|
|
213
217
|
lane.awaiting.splice(lane.awaiting.indexOf(frame), 1);
|
|
214
218
|
const store = entries.get(name)?.store;
|
|
215
|
-
store?.rebase(store.authoritative, { source: "state", through: frame.through });
|
|
219
|
+
store?.rebase(store.authoritative, { source: "state", through: frame.through, cause: "decided" });
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
// Every frame the room ordered at or before the applied seq is in the
|
|
@@ -225,7 +229,7 @@ export function createFastLane({
|
|
|
225
229
|
}
|
|
226
230
|
if (through === undefined) continue;
|
|
227
231
|
const store = entries.get(name)?.store;
|
|
228
|
-
store?.rebase(store.authoritative, { source: "state", through });
|
|
232
|
+
store?.rebase(store.authoritative, { source: "state", through, cause: "settled" });
|
|
229
233
|
}
|
|
230
234
|
}
|
|
231
235
|
|
|
@@ -246,12 +250,14 @@ export function createFastLane({
|
|
|
246
250
|
return;
|
|
247
251
|
}
|
|
248
252
|
frame.seq = Number.isInteger(body?.seq) ? body.seq : null;
|
|
253
|
+
tell(name, { phase: "landed", n: frame.n, seq: frame.seq, through: frame.through });
|
|
249
254
|
// Its event already went by, or came inside a state this tab took: the ops are in.
|
|
250
255
|
settleAwaiting();
|
|
251
256
|
return;
|
|
252
257
|
}
|
|
253
258
|
if (transient(status)) {
|
|
254
259
|
frame.sent = false;
|
|
260
|
+
tell(name, { phase: "retry", code: body?.code ?? (status === 0 ? "offline" : status === 429 ? "rate-limited" : "server"), n: frame.n, through: frame.through, retryAfter: retryAfter ?? null });
|
|
255
261
|
if (unhandled(status)) frame.refused = true;
|
|
256
262
|
else {
|
|
257
263
|
frame.uncertain = true;
|
|
@@ -268,6 +274,7 @@ export function createFastLane({
|
|
|
268
274
|
// The room holds a receipt for every connected sender and has no room for
|
|
269
275
|
// this one: this store goes at save speed until the room takes it back.
|
|
270
276
|
if (status === 503 && body?.code === "busy") {
|
|
277
|
+
tell(name, { phase: "left", code: "busy", n: frame.n, through: frame.through });
|
|
271
278
|
console.warn(`clay.live("${name}"): the room is full, so it stays at save speed for now`);
|
|
272
279
|
roomNames.delete(name);
|
|
273
280
|
leaveRoom(name, lane);
|
|
@@ -285,12 +292,14 @@ export function createFastLane({
|
|
|
285
292
|
return;
|
|
286
293
|
}
|
|
287
294
|
if (REVOKED.has(status)) {
|
|
295
|
+
tell(name, { phase: "refused", code: "revoked", n: frame.n, through: frame.through });
|
|
288
296
|
if (!frame.uncertain) frame.refused = true;
|
|
289
297
|
stop();
|
|
290
298
|
return;
|
|
291
299
|
}
|
|
292
300
|
const reason = body?.error || body?.code || `status ${status}`;
|
|
293
301
|
if (status === 404) {
|
|
302
|
+
tell(name, { phase: "left", code: "no-store", n: frame.n, through: frame.through });
|
|
294
303
|
console.warn(`clay.live("${name}"): the room has no such store, so it stays at save speed (${reason})`);
|
|
295
304
|
frame.refused = true;
|
|
296
305
|
roomNames.delete(name);
|
|
@@ -299,8 +308,9 @@ export function createFastLane({
|
|
|
299
308
|
}
|
|
300
309
|
lane.awaiting.splice(lane.awaiting.indexOf(frame), 1);
|
|
301
310
|
const store = entries.get(name)?.store;
|
|
311
|
+
tell(name, { phase: "refused", code: body?.code ?? "refused", n: frame.n, through: frame.through });
|
|
302
312
|
console.warn(`clay.live("${name}"): the host refused an update, so it was undone (${reason})`);
|
|
303
|
-
store?.rebase(store.authoritative, { source: "state", through: frame.through });
|
|
313
|
+
store?.rebase(store.authoritative, { source: "state", through: frame.through, cause: "refused" });
|
|
304
314
|
}
|
|
305
315
|
|
|
306
316
|
function apply(frame) {
|
|
@@ -321,10 +331,11 @@ export function createFastLane({
|
|
|
321
331
|
const i = lane.awaiting.findIndex((sent) => sent.n === frame.n);
|
|
322
332
|
if (i >= 0) {
|
|
323
333
|
through = lane.awaiting[i].through;
|
|
334
|
+
tell(frame.name, { phase: "echoed", n: frame.n, seq: frame.seq, through });
|
|
324
335
|
lane.awaiting.splice(0, i + 1);
|
|
325
336
|
}
|
|
326
337
|
}
|
|
327
|
-
store.rebase(next, { source: own ? "state" : "remote", through });
|
|
338
|
+
store.rebase(next, { source: own ? "state" : "remote", through, frame: { seq: frame.seq, from: frame.from, n: frame.n, ops: frame.ops } });
|
|
328
339
|
entries.get(frame.name).last = frame.seq;
|
|
329
340
|
onApplied();
|
|
330
341
|
if (frame.checkpoint === true) onCheckpoint();
|
|
@@ -411,7 +422,7 @@ export function createFastLane({
|
|
|
411
422
|
frame.seq = null;
|
|
412
423
|
}
|
|
413
424
|
lane.awaiting = resend;
|
|
414
|
-
entry.store.rebase(room.value, { source: "state", through: through || undefined });
|
|
425
|
+
entry.store.rebase(room.value, { source: "state", through: through || undefined, room: { seq, fresh: true, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null } });
|
|
415
426
|
lane.unsent = unframed(name, lane);
|
|
416
427
|
} else {
|
|
417
428
|
const seen = room.seen ? room.seen[clientId] : undefined;
|
|
@@ -439,7 +450,7 @@ export function createFastLane({
|
|
|
439
450
|
}
|
|
440
451
|
}
|
|
441
452
|
lane.awaiting = next;
|
|
442
|
-
entry.store.rebase(room.value, { source: "state", through });
|
|
453
|
+
entry.store.rebase(room.value, { source: "state", through, room: { seq, fresh: false, moves: state.moves?.[name] ?? null, floor: Number.isInteger(room.floor) ? room.floor : null } });
|
|
443
454
|
// Back in the room after a spell at save speed: frame what was made meanwhile.
|
|
444
455
|
if (rejoined) {
|
|
445
456
|
lane.unsent = unframed(name, lane);
|
|
@@ -476,7 +487,7 @@ export function createFastLane({
|
|
|
476
487
|
const asked = { connection, stateCount };
|
|
477
488
|
let state = null;
|
|
478
489
|
try {
|
|
479
|
-
state = await transport.fetchState(clientId);
|
|
490
|
+
state = await transport.fetchState(clientId, epoch !== null && seq > 0 ? { epoch, seq } : null);
|
|
480
491
|
} catch {}
|
|
481
492
|
fetching = false;
|
|
482
493
|
if (stopped) return;
|
|
@@ -544,7 +555,10 @@ export function createFastLane({
|
|
|
544
555
|
stateCount++;
|
|
545
556
|
for (const [name, landed] of Object.entries(data.landed)) {
|
|
546
557
|
const entry = entries.get(name);
|
|
547
|
-
if (entry && Number.isInteger(landed))
|
|
558
|
+
if (entry && Number.isInteger(landed)) {
|
|
559
|
+
entry.landed = landed;
|
|
560
|
+
tell(name, { phase: "checkpointed", seq: landed });
|
|
561
|
+
}
|
|
548
562
|
}
|
|
549
563
|
onLanded();
|
|
550
564
|
}
|
package/src/live/local-lane.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { applyOps, applyOp, checkOp, cloneJson, validateOp, serializeLive, textHash, diffOps } from "./ops.js";
|
|
22
22
|
import { HyperMorph } from "../vendor/hyper-morph.vendor.js";
|
|
23
|
+
import { movesSince } from "./moves.js";
|
|
23
24
|
|
|
24
25
|
const VERSION = 4;
|
|
25
26
|
export const REQUEST_MS = 2000;
|
|
@@ -118,14 +119,20 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
118
119
|
|
|
119
120
|
const landedIn = (m) => (m.behind || m.landed === null ? -1 : m.landed);
|
|
120
121
|
|
|
121
|
-
function roomState() {
|
|
122
|
+
function roomState(since = null) {
|
|
122
123
|
const stores = {};
|
|
123
124
|
const meta = {};
|
|
124
125
|
for (const [name, m] of mirror.stores) {
|
|
125
126
|
stores[name] = { value: cloneJson(m.value), ephemeral: m.ephemeral, seen: receiptsFor(name), last: m.last, landed: landedIn(m) };
|
|
126
127
|
meta[name] = { log: m.log, last: m.last, landed: m.landed, floor: m.floor, behind: m.behind, file: m.file, inFile: m.inFile };
|
|
127
128
|
}
|
|
128
|
-
|
|
129
|
+
const state = { epoch: room.epoch, seq: room.seq, writer: clientId, stores, mirror: { seen: [...mirror.seen], stores: meta } };
|
|
130
|
+
if (since && since.epoch === mirror.epoch) {
|
|
131
|
+
const moves = {};
|
|
132
|
+
for (const [name, m] of mirror.stores) moves[name] = movesSince(m, since);
|
|
133
|
+
state.moves = moves;
|
|
134
|
+
}
|
|
135
|
+
return state;
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
function alreadyMerged(m, frame) {
|
|
@@ -507,7 +514,7 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
507
514
|
} else if (room && m.type === "post") {
|
|
508
515
|
if (m.epoch === room.epoch) post({ type: "answer", to: m.from, rid: m.rid, result: roomApply(m.from, m.frame) });
|
|
509
516
|
} else if (room && m.type === "state-request") {
|
|
510
|
-
post({ type: "answer", to: m.from, rid: m.rid, result: roomState() });
|
|
517
|
+
post({ type: "answer", to: m.from, rid: m.rid, result: roomState(m.since) });
|
|
511
518
|
}
|
|
512
519
|
}
|
|
513
520
|
|
|
@@ -554,11 +561,11 @@ export function localTransport({ seed, key = "clay-live:" + window.location.path
|
|
|
554
561
|
// The sequencer's state, installed into the mirror first. An answer to a
|
|
555
562
|
// request made before the mirror changed (another state, or this tab
|
|
556
563
|
// starting a room) is dropped: the lane asks again or already has it.
|
|
557
|
-
async fetchState() {
|
|
558
|
-
if (room) return roomState();
|
|
564
|
+
async fetchState(_clientId, since = null) {
|
|
565
|
+
if (room) return roomState(since);
|
|
559
566
|
const asked = gen;
|
|
560
|
-
const state = await request({ type: "state-request" });
|
|
561
|
-
if (room) return roomState();
|
|
567
|
+
const state = await request({ type: "state-request", since });
|
|
568
|
+
if (room) return roomState(since);
|
|
562
569
|
if (asked !== gen || !install(state)) return null;
|
|
563
570
|
return state;
|
|
564
571
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The moves a store's log holds after `since` ({ epoch, seq }), for a tab that
|
|
2
|
+
// already applied everything through since.seq. [] when nothing is newer; null
|
|
3
|
+
// when the log no longer reaches back that far (its floor is past since.seq),
|
|
4
|
+
// so the tab cannot replay and takes the value as it is. A log may start above
|
|
5
|
+
// since.seq + 1 with nothing missing: the seqs between went to other stores.
|
|
6
|
+
export function movesSince(store, since) {
|
|
7
|
+
if (!since || !Number.isInteger(since.seq)) return null;
|
|
8
|
+
if (since.seq >= (store.last ?? 0)) return [];
|
|
9
|
+
if (since.seq < (store.floor ?? 0)) return null;
|
|
10
|
+
return store.log.filter((entry) => entry.seq > since.seq).map((entry) => ({ seq: entry.seq, ops: entry.ops }));
|
|
11
|
+
}
|
package/src/live/server-lane.js
CHANGED
|
@@ -39,9 +39,10 @@ export async function relayData(clientId, frame) {
|
|
|
39
39
|
* The room's { epoch, seq, writer, stores }; { status } when the host refused;
|
|
40
40
|
* null when the request never got an answer. clientId marks this tab as taking part.
|
|
41
41
|
*/
|
|
42
|
-
export async function fetchLiveState(clientId) {
|
|
42
|
+
export async function fetchLiveState(clientId, since = null) {
|
|
43
43
|
const url = dataUrl();
|
|
44
44
|
if (clientId) url.searchParams.set("client-id", clientId);
|
|
45
|
+
if (since && typeof since.epoch === "string" && Number.isInteger(since.seq)) url.searchParams.set("since", `${since.epoch}:${since.seq}`);
|
|
45
46
|
try {
|
|
46
47
|
const response = await fetch(url.href, {
|
|
47
48
|
credentials: "same-origin",
|
package/src/live/store.js
CHANGED
|
@@ -47,6 +47,8 @@ export function createStore({ name, value, ephemeral = false }) {
|
|
|
47
47
|
let mode = "file";
|
|
48
48
|
const listeners = new Set();
|
|
49
49
|
const sinks = new Set();
|
|
50
|
+
const rebases = new Set();
|
|
51
|
+
const deliveries = new Set();
|
|
50
52
|
const views = new WeakMap();
|
|
51
53
|
|
|
52
54
|
function emit(source, ops) {
|
|
@@ -275,17 +277,46 @@ export function createStore({ name, value, ephemeral = false }) {
|
|
|
275
277
|
// `confirmed` pending ops, or every op up to the mark `through`. The rest
|
|
276
278
|
// replay over it; a pending op that no longer applies is dropped too.
|
|
277
279
|
// Listeners hear about it only when the value page code sees changed.
|
|
278
|
-
rebase(next, { source = "remote", confirmed = 0, through } = {}) {
|
|
280
|
+
rebase(next, { source = "remote", confirmed = 0, through, frame = null, room = null, cause = null } = {}) {
|
|
279
281
|
if (!isContainer(next) || kind(next) !== kind(state)) {
|
|
280
282
|
console.warn(`clay.live("${name}"): ignored an incoming value that is not ${kind(state)}`);
|
|
281
283
|
return false;
|
|
282
284
|
}
|
|
283
285
|
const before = serializeLive(state);
|
|
286
|
+
const prev = authoritative;
|
|
284
287
|
authoritative = cloneJson(next);
|
|
285
288
|
pending = through === undefined ? pending.slice(confirmed) : pending.filter((entry) => entry.id > through);
|
|
286
289
|
rebuild();
|
|
290
|
+
for (const fn of [...rebases]) {
|
|
291
|
+
try {
|
|
292
|
+
fn({ source, frame, room, cause, prev, next: authoritative });
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.error(`clay.live("${name}"): a rebase listener threw`, err);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
287
297
|
if (serializeLive(state) !== before) emit(source, null);
|
|
288
298
|
return true;
|
|
289
299
|
},
|
|
300
|
+
// Every rebase, whether or not the value page code sees changed, with what
|
|
301
|
+
// caused it: the frame the room applied, the state read, or the lane's own
|
|
302
|
+
// settling. For the page editor; never on the handle.
|
|
303
|
+
onRebase(fn) {
|
|
304
|
+
rebases.add(fn);
|
|
305
|
+
return () => rebases.delete(fn);
|
|
306
|
+
},
|
|
307
|
+
// What became of this tab's frames: { phase, code, n, seq, through, retryAfter }.
|
|
308
|
+
onDelivery(fn) {
|
|
309
|
+
deliveries.add(fn);
|
|
310
|
+
return () => deliveries.delete(fn);
|
|
311
|
+
},
|
|
312
|
+
deliver(event) {
|
|
313
|
+
for (const fn of [...deliveries]) {
|
|
314
|
+
try {
|
|
315
|
+
fn(event);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
console.error(`clay.live("${name}"): a delivery listener threw`, err);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
},
|
|
290
321
|
};
|
|
291
322
|
}
|
package/src/loader-logic.js
CHANGED
|
@@ -42,6 +42,9 @@ export const PLUGIN_PATHS = {
|
|
|
42
42
|
// Who is editing and who wrote what. Not editOnly: a reader still sees names.
|
|
43
43
|
people: { path: "plugins/people.js", editOnly: false, default: false },
|
|
44
44
|
wire: { path: "plugins/wire.js", editOnly: false, default: false },
|
|
45
|
+
// A visitor's contact form to the document's owner. Not editOnly: visitors are
|
|
46
|
+
// who send messages.
|
|
47
|
+
message: { path: "plugins/message.js", editOnly: false, default: false },
|
|
45
48
|
// Edit mode only: the AI comment box is an editing gesture. Opt in with
|
|
46
49
|
// `plugins=ai-edit`; it stays dormant on any host that does not list a ready
|
|
47
50
|
// `ai-edit` helper.
|
|
@@ -100,6 +103,7 @@ export const MODULES = {
|
|
|
100
103
|
"plugins/upload.js": () => import("./plugins/upload.js"),
|
|
101
104
|
"plugins/people.js": () => import("./plugins/people.js"),
|
|
102
105
|
"plugins/wire.js": () => import("./plugins/wire.js"),
|
|
106
|
+
"plugins/message.js": () => import("./plugins/message.js"),
|
|
103
107
|
"plugins/ai-edit.js": () => import("./plugins/ai-edit.js"),
|
|
104
108
|
"plugins/demo.js": () => import("./plugins/demo.js"),
|
|
105
109
|
"plugins/visitor-drafts.js": () => import("./plugins/visitor-drafts.js"),
|
|
@@ -108,7 +112,7 @@ export const MODULES = {
|
|
|
108
112
|
|
|
109
113
|
// `source` is last on purpose: its install captures a save clone, so it wants every
|
|
110
114
|
// plugin that registers a document transform to have registered it first.
|
|
111
|
-
const PLUGIN_ORDER = ["visitor-drafts", "richclay", "indicator", "sortable", "undo", "quickcrop", "upload", "people", "cms", "sync", "live", "wire", "ai-edit", "demo", "source"];
|
|
115
|
+
const PLUGIN_ORDER = ["visitor-drafts", "richclay", "indicator", "sortable", "undo", "quickcrop", "upload", "people", "cms", "sync", "live", "message", "wire", "ai-edit", "demo", "source"];
|
|
112
116
|
|
|
113
117
|
// A plugin that cannot do its whole job alone. hypercms reads the cropper through
|
|
114
118
|
// a capability lookup (`clay.quickcrop`) and silently uploads the raw file when it
|
package/src/loader.js
CHANGED
|
@@ -141,6 +141,8 @@ function attachPluginMember(path, mod, loaded) {
|
|
|
141
141
|
clay.people = mod.people;
|
|
142
142
|
clay.author = mod.author;
|
|
143
143
|
Object.defineProperty(clay, "me", { get: mod.currentMe, configurable: true, enumerable: true });
|
|
144
|
+
} else if (path === "plugins/message.js") {
|
|
145
|
+
clay.message = mod.message;
|
|
144
146
|
} else if (path === "plugins/wire.js") {
|
|
145
147
|
clay.wire = mod.wire || mod.default;
|
|
146
148
|
} else if (path === "plugins/ai-edit.js") {
|