a proof of concept realtime collaborative todo list
1// Each item record carries a $crdt map: { <field>: { clock, actor } }.
2// Merge picks per-field winner by highest clock, lex-greater actor as tiebreak.
3
4export function mergeRecords(records) {
5 if (records.length === 0) return null;
6 const fields = new Set();
7 for (const r of records) for (const k of Object.keys(r.$crdt || {})) fields.add(k);
8
9 const out = { $crdt: {} };
10 for (const field of fields) {
11 let best = null;
12 for (const r of records) {
13 const meta = r.$crdt?.[field];
14 if (!meta) continue;
15 if (
16 !best ||
17 meta.clock > best.meta.clock ||
18 (meta.clock === best.meta.clock && meta.actor > best.meta.actor)
19 ) {
20 best = { value: r[field], meta };
21 }
22 }
23 if (best) {
24 out[field] = best.value;
25 out.$crdt[field] = best.meta;
26 }
27 }
28 return out;
29}
30
31// Build the record we'll write into our own repo. Each editor's record stays
32// fully up-to-date: copy every field from the merged state, then bump $crdt
33// (clock + actor) for the fields we're explicitly changing.
34export function crdtEqual(a, b) {
35 const ak = Object.keys(a || {});
36 const bk = Object.keys(b || {});
37 if (ak.length !== bk.length) return false;
38 for (const k of ak) {
39 const x = a[k];
40 const y = b[k];
41 if (!y || x.clock !== y.clock || x.actor !== y.actor) return false;
42 }
43 return true;
44}
45
46export function applyUpdates(merged, updates, actor) {
47 const record = merged ? { ...merged, $crdt: { ...(merged.$crdt || {}) } } : { $crdt: {} };
48 for (const [field, value] of Object.entries(updates)) {
49 record[field] = value;
50 const prevClock = merged?.$crdt?.[field]?.clock ?? 0;
51 record.$crdt[field] = { clock: prevClock + 1, actor };
52 }
53 return record;
54}