@panphora/clayjs 1.13.1 → 1.14.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.
Files changed (136) hide show
  1. package/THIRD-PARTY-NOTICES.md +19 -0
  2. package/dist/clay.standalone.js +69667 -41708
  3. package/dist/server/checkpoint.cjs +70690 -0
  4. package/dist/server/checkpoint.mjs +8 -0
  5. package/dist/server/rooms.cjs +72039 -0
  6. package/dist/server/rooms.mjs +66 -0
  7. package/docs/guide.md +94 -93
  8. package/entries/clay.js +22 -0
  9. package/package.json +16 -2
  10. package/packed-contract.json +17 -3
  11. package/src/core/conflict-download.js +11 -2
  12. package/src/core/host-meta.js +75 -0
  13. package/src/core/persist.js +11 -0
  14. package/src/core/save-core.js +266 -31
  15. package/src/core/save.js +62 -32
  16. package/src/core/snapshot.js +39 -13
  17. package/src/core/style-comparisons.js +37 -0
  18. package/src/dom/insert-style-tag.js +11 -12
  19. package/src/lib/effect.js +130 -0
  20. package/src/lib/style-adoption.js +9 -0
  21. package/src/lib/style.js +44 -0
  22. package/src/live/fast-lane.js +347 -60
  23. package/src/live/local-lane.js +32 -6
  24. package/src/live/ops.js +77 -0
  25. package/src/live/provenance.js +129 -0
  26. package/src/live/save-history.js +188 -0
  27. package/src/live/server-lane.js +9 -2
  28. package/src/live/store.js +194 -16
  29. package/src/loader-logic.js +7 -3
  30. package/src/loader.js +6 -0
  31. package/src/page/address.js +20 -0
  32. package/src/page/adopt-boot.js +189 -0
  33. package/src/page/adopt.js +240 -0
  34. package/src/page/ai-tool.js +188 -0
  35. package/src/page/announce.js +21 -0
  36. package/src/page/blocks.js +1121 -0
  37. package/src/page/blocksel.js +269 -0
  38. package/src/page/board-drag.js +226 -0
  39. package/src/page/board-panel.js +353 -0
  40. package/src/page/board-style.js +146 -0
  41. package/src/page/board-view.js +447 -0
  42. package/src/page/board.js +41 -0
  43. package/src/page/chart.js +220 -0
  44. package/src/page/checklist.js +143 -0
  45. package/src/page/chrome.js +587 -0
  46. package/src/page/csv-import.js +169 -0
  47. package/src/page/drag.js +202 -0
  48. package/src/page/drop.js +112 -0
  49. package/src/page/editor.js +384 -0
  50. package/src/page/highlight.js +268 -0
  51. package/src/page/inline.js +60 -1
  52. package/src/page/items.js +159 -0
  53. package/src/page/link.js +247 -0
  54. package/src/page/markdown.js +71 -0
  55. package/src/page/media.js +570 -0
  56. package/src/page/merge.js +130 -0
  57. package/src/page/model.js +71 -9
  58. package/src/page/placeholder.js +23 -0
  59. package/src/page/projection.js +647 -0
  60. package/src/page/recover.js +193 -0
  61. package/src/page/rich/cell.js +16 -0
  62. package/src/page/rich/chart.js +104 -0
  63. package/src/page/rich/csv.js +118 -0
  64. package/src/page/rich/page-tool.js +86 -0
  65. package/src/page/rich/tool-runtime.js +195 -0
  66. package/src/page/save-projection.js +53 -0
  67. package/src/page/save-states.js +85 -0
  68. package/src/page/schema.js +12 -5
  69. package/src/page/table-kinds.js +20 -0
  70. package/src/page/table-paste.js +57 -0
  71. package/src/page/table-select.js +244 -0
  72. package/src/page/table-style.js +25 -0
  73. package/src/page/table.js +650 -0
  74. package/src/page/themes/content.css +46 -0
  75. package/src/page/themes/light-1.css +269 -0
  76. package/src/page/themes/light-2.css +211 -0
  77. package/src/page/themes/light-3.css +130 -0
  78. package/src/page/themes/light-4.css +299 -0
  79. package/src/page/tool-data.js +25 -0
  80. package/src/page/tool-view.js +68 -0
  81. package/src/page/tools.js +133 -0
  82. package/src/page/view.js +136 -0
  83. package/src/page/ybind/author.js +25 -0
  84. package/src/page/ybind/binding.js +92 -0
  85. package/src/page/ybind/board-commands.js +170 -0
  86. package/src/page/ybind/board.js +89 -0
  87. package/src/page/ybind/bridge.js +382 -0
  88. package/src/page/ybind/conversion.js +26 -0
  89. package/src/page/ybind/core.js +7 -0
  90. package/src/page/ybind/decode.js +208 -0
  91. package/src/page/ybind/doc.js +58 -0
  92. package/src/page/ybind/external.js +190 -0
  93. package/src/page/ybind/group.js +124 -0
  94. package/src/page/ybind/index.js +12 -0
  95. package/src/page/ybind/layout.js +27 -0
  96. package/src/page/ybind/marks.js +18 -0
  97. package/src/page/ybind/migrate.js +152 -0
  98. package/src/page/ybind/moves.js +288 -0
  99. package/src/page/ybind/origins.js +257 -0
  100. package/src/page/ybind/parts.js +44 -0
  101. package/src/page/ybind/projection-map.js +92 -0
  102. package/src/page/ybind/provider.js +285 -0
  103. package/src/page/ybind/recovery.js +411 -0
  104. package/src/page/ybind/seed.js +27 -0
  105. package/src/page/ybind/table.js +166 -0
  106. package/src/page/ybind/tokens.js +93 -0
  107. package/src/page/ybind/walk.js +333 -0
  108. package/src/page/ybind/writer.js +307 -0
  109. package/src/plugins/indicator.js +16 -6
  110. package/src/plugins/live.js +742 -65
  111. package/src/plugins/page.js +547 -0
  112. package/src/plugins/undo.js +60 -3
  113. package/src/server/checkpoint/index.js +218 -0
  114. package/src/server/rooms/index.js +59 -0
  115. package/src/server/rooms/live-data-ops.js +369 -0
  116. package/src/server/rooms/live-data-rooms.js +965 -0
  117. package/src/server/rooms/live-data-tags.js +49 -0
  118. package/src/server/rooms/live-page-history.js +41 -0
  119. package/src/server/rooms/live-provenance.js +142 -0
  120. package/src/server/rooms/page-admission.js +79 -0
  121. package/src/server/rooms/page-conversion.js +62 -0
  122. package/src/server/rooms/page-rebase.js +108 -0
  123. package/src/server/rooms/page-state.js +91 -0
  124. package/src/server/rooms/page-write-admission.js +63 -0
  125. package/src/server/rooms/page-write-guard.js +23 -0
  126. package/src/server/rooms/recent.js +18 -0
  127. package/src/server/rooms/spec-wire.js +17 -0
  128. package/src/standalone.js +22 -0
  129. package/src/sync/live-sync.js +13 -2
  130. package/src/sync/stream.js +62 -16
  131. package/src/ui/index.js +4 -0
  132. package/src/ui/toast.js +21 -5
  133. package/src/vendor/pierre/pierre.vendor.js +3 -0
  134. package/src/vendor/yjs.vendor.js +7 -0
  135. package/src/page/binding.js +0 -647
  136. package/src/page/replay.js +0 -578
@@ -0,0 +1,92 @@
1
+ import * as Y from "../../vendor/yjs.vendor.js";
2
+ import { NET, createPageDoc, walk, docOf } from "./core.js";
3
+ import { createBridge, keepViewPos } from "./bridge.js";
4
+ import { createYProvider, entriesOf, dec } from "./provider.js";
5
+
6
+ export function createYPageBinding({ store, ydoc = new Y.Doc(), onNotice = () => {}, ownedTokens = [], authorToken = null, setTimer = setTimeout, clearTimer = clearTimeout, compact, captureTimeout = 500, checkpointWithin = () => {}, requestCheckpoint = null, registerUnsavedState = null, onStopped = () => {} }) {
7
+ for (const [, v] of entriesOf(store.value.y)) Y.applyUpdateV2(ydoc, dec(v), NET);
8
+ const pd = createPageDoc(ydoc);
9
+ const provider = createYProvider({ store, ydoc, setTimer, clearTimer, compact, onStopped });
10
+ const stats = { groups: 0, mismatch: 0, renders: 0, first: null };
11
+ const bridge = createBridge({ pd, onNotice, ownedTokens, authorToken, stats, captureTimeout });
12
+ const waiters = new Set();
13
+ let disposed = false;
14
+ let checksQueued = false;
15
+ const unsaved = registerUnsavedState?.({ id: "page:live", isPending: () => store.pending.length > 0 || provider.reconcilePending, flush });
16
+ const changed = () => {
17
+ unsaved?.changed();
18
+ if (checksQueued) return;
19
+ checksQueued = true;
20
+ Promise.resolve().then(() => {
21
+ checksQueued = false;
22
+ if (!disposed) for (const waiter of [...waiters]) waiter.check();
23
+ });
24
+ };
25
+ const offLocal = store.onLocal(() => { checkpointWithin(1500); changed(); });
26
+ const offRebase = store.onRebase(changed);
27
+ const offConfirm = store.onConfirm(changed);
28
+
29
+ function flush({ timeoutMs = 10_000 } = {}) {
30
+ if (disposed) return Promise.resolve({ ok: false, code: "disposed" });
31
+ return new Promise((resolve) => {
32
+ const deadline = Date.now() + timeoutMs;
33
+ let done = false, checking = false, checkpointing = false, timer = null;
34
+ const waiter = { check, finish };
35
+ function finish(result) {
36
+ if (done) return;
37
+ done = true;
38
+ waiters.delete(waiter);
39
+ clearTimer(timer);
40
+ resolve(result);
41
+ }
42
+ function check() {
43
+ if (done || checking || checkpointing) return;
44
+ checking = true;
45
+ try {
46
+ provider.settle();
47
+ if (store.pending.length) {
48
+ checkpointWithin(0);
49
+ if (store.pending.length) return;
50
+ }
51
+ checkpointing = true;
52
+ if (!requestCheckpoint) { finish({ ok: true }); return; }
53
+ Promise.resolve().then(() => {
54
+ if (!done) return requestCheckpoint({ timeoutMs: Math.max(0, deadline - Date.now()) });
55
+ }).then(finish, (error) => finish({ ok: false, code: "checkpoint-failed", message: error?.message || String(error) }));
56
+ } catch (error) {
57
+ finish({ ok: false, code: "flush-failed", message: error?.message || String(error) });
58
+ } finally { checking = false; }
59
+ }
60
+ waiters.add(waiter);
61
+ timer = setTimer(() => finish({ ok: false, code: "timeout" }), timeoutMs);
62
+ check();
63
+ });
64
+ }
65
+
66
+ return {
67
+ ydoc, pd, provider, bridge, stats, flush,
68
+ doc: () => keepViewPos(docOf(pd, walk(pd))),
69
+ attach: (view) => bridge.attach(view),
70
+ dispatch: (tr) => bridge.dispatch(tr),
71
+ moveBlocks: (args) => bridge.moveBlocks(args),
72
+ board: bridge.board,
73
+ undo: () => bridge.undo(),
74
+ redo: () => bridge.redo(),
75
+ restore: (sourceKey, revision) => bridge.restore(sourceKey, revision),
76
+ get recoverable() { return bridge.recoverable; },
77
+ get notices() { return bridge.notices; },
78
+ subscribeRecovery: (listener) => bridge.subscribeRecovery(listener),
79
+ dismiss: (sourceKey, revision) => bridge.dismiss(sourceKey, revision),
80
+ dispose() {
81
+ if (disposed) return;
82
+ disposed = true;
83
+ offLocal();
84
+ offRebase();
85
+ offConfirm();
86
+ unsaved?.dispose();
87
+ for (const waiter of [...waiters]) waiter.finish({ ok: false, code: "disposed" });
88
+ bridge.dispose();
89
+ provider.dispose();
90
+ },
91
+ };
92
+ }
@@ -0,0 +1,170 @@
1
+ import { schema } from "../schema.js";
2
+ import { isKey, keyBetween, newId, orderedIds } from "../model.js";
3
+ import { canon } from "../projection.js";
4
+ import { LOCAL } from "./layout.js";
5
+ import { walk } from "./walk.js";
6
+ import { fieldsOf } from "./doc.js";
7
+ import { attrKey, recoveredSource } from "./recovery.js";
8
+ import { removedBoardEntities, writeBoard } from "./board.js";
9
+
10
+ const refuse = (code) => ({ status: "refused", code });
11
+ const copy = (value) => JSON.parse(JSON.stringify(value));
12
+ const CARD_FIELDS = ["title", "owner", "due"];
13
+ const DUE = /^\d{4}-\d{2}-\d{2}$/;
14
+ const validValue = (field, value) => {
15
+ if (field === "title") return typeof value === "string";
16
+ if (field === "due") return value === null || (typeof value === "string" && DUE.test(value));
17
+ return value === null || (typeof value === "string" && value.length > 0);
18
+ };
19
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
20
+
21
+ export function slotKey(positions, index) {
22
+ const lower = index > 0 ? positions[index - 1] : null;
23
+ const upper = positions.slice(index).find((pos) => lower === null || pos > lower) ?? null;
24
+ const fits = (key) => (lower === null || key > lower) && (upper === null || key < upper);
25
+ for (const [a, b] of [[lower, upper], [null, upper], [lower, null]]) {
26
+ if ((a !== null && !isKey(a)) || (b !== null && !isKey(b)) || (a !== null && b !== null && a >= b)) continue;
27
+ const key = keyBetween(a, b);
28
+ if (fits(key)) return key;
29
+ }
30
+ return `${positions.reduce((max, pos) => (pos > max ? pos : max), "")}V`;
31
+ }
32
+
33
+ export function placeAt(ids, posOf, index) {
34
+ const positions = ids.map(posOf);
35
+ const lower = index > 0 ? positions[index - 1] : null;
36
+ let end = index;
37
+ while (lower !== null && end < positions.length && positions[end] === lower) end++;
38
+ if (end === index) return { pos: slotKey(positions, index), rekey: [] };
39
+ const upper = positions.slice(end).find((pos) => pos > lower) ?? null;
40
+ const keys = [];
41
+ for (let i = index, prev = lower; i <= end; i++) keys.push(prev = slotKey(upper === null ? [prev] : [prev, upper], 1));
42
+ return { pos: keys[0], rekey: ids.slice(index, end).map((id, i) => [id, keys[i + 1]]) };
43
+ }
44
+
45
+ const place = (map, ids, index) => {
46
+ const { pos, rekey } = placeAt(ids, (id) => map[id].pos, index);
47
+ for (const [id, next] of rekey) map[id] = { ...map[id], pos: next };
48
+ return pos;
49
+ };
50
+
51
+ export function createBoardCommands({ pd, um = null, origins = null, render = () => {} }) {
52
+ const boardAt = (blockId) => {
53
+ if (typeof blockId !== "string") return null;
54
+ const seg = walk(pd, origins).visible.find((candidate) => candidate.id === blockId);
55
+ if (!seg || seg.t !== "board") return null;
56
+ const fields = seg.fields ?? fieldsOf(pd, seg, schema.nodes.board);
57
+ return { columns: copy(fields.columns ?? {}), cards: copy(fields.cards ?? {}) };
58
+ };
59
+ const commit = (blockId, before, after, result) => {
60
+ const source = recoveredSource(blockId);
61
+ um?.stopCapturing();
62
+ pd.ydoc.transact(() => {
63
+ if (!source) writeBoard(pd, blockId, before, after);
64
+ else for (const attr of ["columns", "cards"]) if (canon(before[attr]) !== canon(after[attr])) pd.gone.set(attrKey(source, attr), copy(after[attr]));
65
+ }, LOCAL);
66
+ um?.stopCapturing();
67
+ render();
68
+ return result;
69
+ };
70
+ const cardsIn = (board, columnId, without = null) => orderedIds(Object.fromEntries(Object.entries(board.cards).filter(([id, card]) => card.col === columnId && id !== without)));
71
+ const indexAfter = (ids, afterId) => (afterId === undefined ? ids.length : afterId === null ? 0 : ids.indexOf(afterId) + 1);
72
+ const badAfter = (ids, afterId) => afterId !== undefined && afterId !== null && !ids.includes(afterId);
73
+ const withBoard = (blockId, run) => {
74
+ const board = boardAt(blockId);
75
+ return board ? run(board, copy(board)) : refuse("missing-board");
76
+ };
77
+
78
+ return {
79
+ read: (blockId) => boardAt(blockId),
80
+ removed(blockId) {
81
+ if (!boardAt(blockId)) return null;
82
+ return recoveredSource(blockId) ? { columns: {}, cards: {} } : removedBoardEntities(pd.boardFields, blockId);
83
+ },
84
+ createColumn: (blockId, { name = "", afterId } = {}) => withBoard(blockId, (board, after) => {
85
+ if (typeof name !== "string") return refuse("invalid-value");
86
+ const ids = orderedIds(board.columns);
87
+ if (badAfter(ids, afterId)) return refuse("missing-destination");
88
+ const id = newId("c");
89
+ after.columns[id] = { pos: place(after.columns, ids, indexAfter(ids, afterId)), name };
90
+ return commit(blockId, board, after, { status: "created", id });
91
+ }),
92
+ renameColumn: (blockId, columnId, name) => withBoard(blockId, (board, after) => {
93
+ if (!Object.hasOwn(board.columns, columnId)) return refuse("missing-column");
94
+ if (typeof name !== "string") return refuse("invalid-value");
95
+ if (board.columns[columnId].name === name) return { status: "unchanged" };
96
+ after.columns[columnId].name = name;
97
+ return commit(blockId, board, after, { status: "changed" });
98
+ }),
99
+ moveColumn: (blockId, columnId, { afterId = null } = {}) => withBoard(blockId, (board, after) => {
100
+ if (!Object.hasOwn(board.columns, columnId)) return refuse("missing-column");
101
+ const ids = orderedIds(board.columns).filter((id) => id !== columnId);
102
+ if (badAfter(ids, afterId)) return refuse("missing-destination");
103
+ const index = indexAfter(ids, afterId);
104
+ const current = orderedIds(board.columns);
105
+ if ([...ids.slice(0, index), columnId, ...ids.slice(index)].every((id, i) => id === current[i])) return { status: "unchanged" };
106
+ after.columns[columnId].pos = place(after.columns, ids, index);
107
+ return commit(blockId, board, after, { status: "moved" });
108
+ }),
109
+ removeColumn: (blockId, columnId) => withBoard(blockId, (board, after) => {
110
+ if (!Object.hasOwn(board.columns, columnId)) return refuse("missing-column");
111
+ delete after.columns[columnId];
112
+ return commit(blockId, board, after, { status: "removed", cards: cardsIn(board, columnId).length });
113
+ }),
114
+ restoreColumn: (blockId, columnId) => withBoard(blockId, (board, after) => {
115
+ if (Object.hasOwn(board.columns, columnId)) return { status: "unchanged" };
116
+ const removed = recoveredSource(blockId) ? null : removedBoardEntities(pd.boardFields, blockId).columns[columnId];
117
+ if (!removed) return refuse("missing-column");
118
+ after.columns[columnId] = removed;
119
+ return commit(blockId, board, after, { status: "restored" });
120
+ }),
121
+ createCard: (blockId, { columnId, title = "", owner = null, due = null, afterId } = {}) => withBoard(blockId, (board, after) => {
122
+ if (!Object.hasOwn(board.columns, columnId)) return refuse("missing-column");
123
+ if (!validValue("title", title) || !validValue("owner", owner) || !validValue("due", due)) return refuse("invalid-value");
124
+ const ids = cardsIn(board, columnId);
125
+ if (badAfter(ids, afterId)) return refuse("missing-destination");
126
+ const id = newId("k");
127
+ after.cards[id] = { pos: place(after.cards, ids, indexAfter(ids, afterId)), col: columnId, title, ...(owner !== null ? { owner } : {}), ...(due !== null ? { due } : {}) };
128
+ return commit(blockId, board, after, { status: "created", id });
129
+ }),
130
+ editCard: (blockId, cardId, patch) => withBoard(blockId, (board, after) => {
131
+ if (!Object.hasOwn(board.cards, cardId)) return refuse("missing-card");
132
+ if (!record(patch) || !Object.keys(patch).length || Object.keys(patch).some((field) => !CARD_FIELDS.includes(field))) return refuse("invalid-field");
133
+ if (Object.entries(patch).some(([field, value]) => !validValue(field, value))) return refuse("invalid-value");
134
+ const next = { ...board.cards[cardId] };
135
+ for (const [field, value] of Object.entries(patch)) {
136
+ if (value === null) delete next[field];
137
+ else next[field] = value;
138
+ }
139
+ if (canon(next) === canon(board.cards[cardId])) return { status: "unchanged" };
140
+ after.cards[cardId] = next;
141
+ return commit(blockId, board, after, { status: "changed" });
142
+ }),
143
+ moveCard: (blockId, cardId, { columnId, afterId = null } = {}) => withBoard(blockId, (board, after) => {
144
+ if (!Object.hasOwn(board.cards, cardId)) return refuse("missing-card");
145
+ if (!Object.hasOwn(board.columns, columnId)) return refuse("missing-column");
146
+ const ids = cardsIn(board, columnId, cardId);
147
+ if (badAfter(ids, afterId)) return refuse("missing-destination");
148
+ const index = indexAfter(ids, afterId);
149
+ const card = board.cards[cardId];
150
+ if (card.col === columnId) {
151
+ const current = cardsIn(board, columnId);
152
+ if ([...ids.slice(0, index), cardId, ...ids.slice(index)].every((id, i) => id === current[i])) return { status: "unchanged" };
153
+ }
154
+ after.cards[cardId] = { ...card, col: columnId, pos: place(after.cards, ids, index) };
155
+ return commit(blockId, board, after, { status: "moved" });
156
+ }),
157
+ removeCard: (blockId, cardId) => withBoard(blockId, (board, after) => {
158
+ if (!Object.hasOwn(board.cards, cardId)) return refuse("missing-card");
159
+ delete after.cards[cardId];
160
+ return commit(blockId, board, after, { status: "removed" });
161
+ }),
162
+ restoreCard: (blockId, cardId) => withBoard(blockId, (board, after) => {
163
+ if (Object.hasOwn(board.cards, cardId)) return { status: "unchanged" };
164
+ const removed = recoveredSource(blockId) ? null : removedBoardEntities(pd.boardFields, blockId).cards[cardId];
165
+ if (!removed) return refuse("missing-card");
166
+ after.cards[cardId] = removed;
167
+ return commit(blockId, board, after, { status: "restored" });
168
+ }),
169
+ };
170
+ }
@@ -0,0 +1,89 @@
1
+ import { canon, orderedIds } from "../projection.js";
2
+ import { YDecodeError } from "./provider.js";
3
+
4
+ export const BOARD_FIELDS = Object.freeze({ column: Object.freeze(["alive", "name", "pos"]), card: Object.freeze(["alive", "title", "owner", "due", "placement"]) });
5
+ const FIELDS = { column: new Set(BOARD_FIELDS.column), card: new Set(BOARD_FIELDS.card) };
6
+ const ENTITIES = [["column", "columns"], ["card", "cards"]];
7
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
8
+ const validId = (id) => typeof id === "string" && id.length > 0 && !["__proto__", "constructor", "prototype"].includes(id);
9
+ const fail = (key) => { throw new YDecodeError("unsupported-content", `Unsupported board field: ${key}`); };
10
+ const copy = (value) => (value === null || typeof value !== "object" ? value : JSON.parse(JSON.stringify(value)));
11
+
12
+ export const boardKey = (blockId, kind, entityId, field) => JSON.stringify([blockId, kind, entityId, field]);
13
+
14
+ function entitiesOf(fields, blockId) {
15
+ const groups = new Map();
16
+ for (const [key, value] of fields) {
17
+ let tuple;
18
+ try { tuple = JSON.parse(key); } catch { fail(key); }
19
+ if (!Array.isArray(tuple) || tuple.length !== 4) fail(key);
20
+ const [owner, kind, entityId, field] = tuple;
21
+ if (!validId(owner) || !validId(entityId) || !Object.hasOwn(FIELDS, kind) || !FIELDS[kind].has(field)) fail(key);
22
+ if (field === "alive" ? typeof value !== "boolean"
23
+ : field === "placement" ? !record(value) || Object.keys(value).sort().join(",") !== "columnId,pos" || typeof value.columnId !== "string" || typeof value.pos !== "string"
24
+ : field === "owner" || field === "due" ? value !== null && typeof value !== "string"
25
+ : typeof value !== "string") fail(key);
26
+ if (owner !== blockId) continue;
27
+ const bucket = groups.get(kind) ?? new Map();
28
+ const entity = bucket.get(entityId) ?? {};
29
+ entity[field] = value;
30
+ bucket.set(entityId, entity);
31
+ groups.set(kind, bucket);
32
+ }
33
+ return groups;
34
+ }
35
+
36
+ const columnOf = (item) => ({ pos: item.pos ?? "", name: item.name ?? "" });
37
+ const cardOf = (item) => ({ pos: item.placement?.pos ?? "", col: item.placement?.columnId ?? "", title: item.title ?? "", ...(item.owner != null ? { owner: item.owner } : {}), ...(item.due != null ? { due: item.due } : {}) });
38
+ const pick = (groups, kind, shape, alive) => Object.fromEntries([...(groups.get(kind) ?? [])].filter(([, item]) => (item.alive !== false) === alive).map(([id, item]) => [id, shape(item)]));
39
+
40
+ export function resolveBoardFields(fields, blockId = null) {
41
+ const groups = entitiesOf(fields, blockId);
42
+ const columns = pick(groups, "column", columnOf, true);
43
+ const cards = pick(groups, "card", cardOf, true);
44
+ const columnIds = orderedIds(columns);
45
+ const missing = [...new Set(Object.values(cards).map((card) => card.col))].filter((id) => !Object.hasOwn(columns, id)).sort();
46
+ const cardIds = [...columnIds, ...missing].flatMap((columnId) => orderedIds(Object.fromEntries(Object.entries(cards).filter(([, card]) => card.col === columnId))));
47
+ return {
48
+ columns: Object.fromEntries(columnIds.map((id) => [id, columns[id]])),
49
+ cards: Object.fromEntries(cardIds.map((id) => [id, cards[id]])),
50
+ };
51
+ }
52
+
53
+ export function removedBoardEntities(fields, blockId) {
54
+ const groups = entitiesOf(fields, blockId);
55
+ return { columns: pick(groups, "column", columnOf, false), cards: pick(groups, "card", cardOf, false) };
56
+ }
57
+
58
+ const DEFAULTS = { name: "", pos: "", title: "", owner: null, due: null, placement: { columnId: "", pos: "" } };
59
+ const fieldOf = (kind, entity, field) => {
60
+ if (kind === "card" && field === "placement") return { columnId: entity.col ?? "", pos: entity.pos ?? "" };
61
+ return entity[field] ?? DEFAULTS[field];
62
+ };
63
+
64
+ export function boardChanges(blockId, fields, before, after) {
65
+ const out = [];
66
+ for (const [kind, attr] of ENTITIES) {
67
+ const was = record(before?.[attr]) ? before[attr] : {}, now = record(after?.[attr]) ? after[attr] : {};
68
+ for (const id of new Set([...Object.keys(was), ...Object.keys(now)])) {
69
+ const old = was[id], next = now[id];
70
+ if (!next) {
71
+ if (old) out.push([boardKey(blockId, kind, id, "alive"), false]);
72
+ continue;
73
+ }
74
+ if (!old) out.push([boardKey(blockId, kind, id, "alive"), true]);
75
+ for (const field of BOARD_FIELDS[kind]) {
76
+ if (field === "alive") continue;
77
+ const key = boardKey(blockId, kind, id, field);
78
+ const value = fieldOf(kind, next, field);
79
+ const prior = old ? fieldOf(kind, old, field) : fields.has(key) ? fields.get(key) : DEFAULTS[field];
80
+ if (canon(prior) !== canon(value)) out.push([key, copy(value)]);
81
+ }
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+
87
+ export function writeBoard(pd, blockId, before, after) {
88
+ for (const [key, value] of boardChanges(blockId, pd.boardFields, before, after)) pd.boardFields.set(key, value);
89
+ }