@panphora/clayjs 1.12.2 → 1.13.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/dist/clay.standalone.js +3235 -2997
- package/docs/guide.md +1847 -0
- package/entries/sap.js +6 -6
- package/package.json +3 -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/page/binding.js +647 -0
- package/src/page/inline.js +113 -0
- package/src/page/model.js +49 -21
- package/src/page/replay.js +578 -0
- package/src/page/schema.js +1 -1
- package/src/plugins/live.js +49 -2
- package/src/plugins/visitor-drafts/prompt.js +4 -6
- package/src/vendor/hypercms.vendor.js +20 -20
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
|
}
|