@echovisionlab/geul-common 0.1.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/LICENSE.md +6 -0
- package/README.md +37 -0
- package/package.json +89 -0
- package/src/collaboration/artist.ts +63 -0
- package/src/collaboration/block-room-codec/ai-document-applicator.ts +319 -0
- package/src/collaboration/block-room-codec/ai-document-field-mutations.ts +557 -0
- package/src/collaboration/block-room-codec/ai-document-page-structure-mutations.ts +449 -0
- package/src/collaboration/block-room-codec/ai-document-values.ts +368 -0
- package/src/collaboration/block-room-codec/hydration.ts +257 -0
- package/src/collaboration/block-room-codec/internal.ts +432 -0
- package/src/collaboration/block-room-codec/locale-change-validation.ts +237 -0
- package/src/collaboration/block-room-codec/locale-presence.ts +827 -0
- package/src/collaboration/block-room-codec/materialization.ts +450 -0
- package/src/collaboration/block-room-codec/observation.ts +518 -0
- package/src/collaboration/block-room-codec/payload-mutations.ts +347 -0
- package/src/collaboration/block-room-codec/room-access.ts +154 -0
- package/src/collaboration/block-room-codec/structure-mutations.ts +456 -0
- package/src/collaboration/block-room-codec.ts +86 -0
- package/src/collaboration/campaign.ts +37 -0
- package/src/collaboration/document-layout.ts +75 -0
- package/src/collaboration/document.ts +185 -0
- package/src/collaboration/email-layout.ts +150 -0
- package/src/collaboration/form.ts +513 -0
- package/src/collaboration/label.ts +49 -0
- package/src/collaboration/map-theme.ts +157 -0
- package/src/collaboration/member-id.ts +9 -0
- package/src/collaboration/menu.ts +258 -0
- package/src/collaboration/metadata-ai.ts +99 -0
- package/src/collaboration/page.ts +483 -0
- package/src/collaboration/post-series.ts +96 -0
- package/src/collaboration/post.ts +59 -0
- package/src/collaboration/release.ts +221 -0
- package/src/collaboration/runtime-events.ts +547 -0
- package/src/collaboration/work.ts +107 -0
- package/src/editor/link-normalization.ts +212 -0
- package/src/editor/materialized-blocks.ts +58 -0
- package/src/index.ts +22 -0
- package/src/media/block-schemas.ts +78 -0
- package/src/media/hydration.ts +226 -0
- package/src/page/block-fixtures.ts +586 -0
- package/src/page/index.ts +3 -0
- package/src/page/types.ts +57 -0
- package/src/post/index.ts +1 -0
- package/src/post/types.ts +13 -0
- package/src/test/random-id.ts +9 -0
- package/src/translation/release.ts +88 -0
- package/src/types.ts +14 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import type { AIDocumentFieldTarget } from "@echovisionlab/geul-proto/secure/ai_pb.ts";
|
|
3
|
+
import {
|
|
4
|
+
BASE_NODE_KEYS,
|
|
5
|
+
LOCALE_NODE_KEYS,
|
|
6
|
+
fail,
|
|
7
|
+
orderContainerKey,
|
|
8
|
+
stringValue,
|
|
9
|
+
yMap,
|
|
10
|
+
type BlockRoomDocumentType,
|
|
11
|
+
type BlockRoomTypedDocument,
|
|
12
|
+
} from "./internal.ts";
|
|
13
|
+
import {
|
|
14
|
+
blockRoomBaseNodes,
|
|
15
|
+
blockRoomBaseOrder,
|
|
16
|
+
blockRoomLocaleOverlay,
|
|
17
|
+
decodeCanonicalBlockRoom,
|
|
18
|
+
nodePayload,
|
|
19
|
+
type BlockRoomBaseNodeSnapshot,
|
|
20
|
+
type BlockRoomLocaleNodeSnapshot,
|
|
21
|
+
type CanonicalBlockRoomSnapshot,
|
|
22
|
+
} from "./materialization.ts";
|
|
23
|
+
import { roomDocumentType } from "./room-access.ts";
|
|
24
|
+
import {
|
|
25
|
+
blockRoomLocaleValue,
|
|
26
|
+
blockRoomLocalePresence,
|
|
27
|
+
blockRoomPresentLocaleValues,
|
|
28
|
+
blockRoomLocaleValueTargetIdentity,
|
|
29
|
+
blockRoomLocaleValueTargetBlockId,
|
|
30
|
+
canonicalBlockRoomLocaleValueTargetKey,
|
|
31
|
+
localePresenceTargetsForBlock,
|
|
32
|
+
sparseBlockRoomLocalePayload,
|
|
33
|
+
} from "./locale-presence.ts";
|
|
34
|
+
|
|
35
|
+
export interface CanonicalBlockRoomTransaction {
|
|
36
|
+
document: BlockRoomTypedDocument;
|
|
37
|
+
snapshot: CanonicalBlockRoomSnapshot;
|
|
38
|
+
origin: unknown;
|
|
39
|
+
local: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BlockRoomChangeSet {
|
|
43
|
+
affectedBaseBlockIds: string[];
|
|
44
|
+
affectedLocaleBlockIds: string[];
|
|
45
|
+
affectedLocaleValueTargets: AIDocumentFieldTarget[];
|
|
46
|
+
changedContainerOrderKeys: string[];
|
|
47
|
+
documentMetadataChanged: boolean;
|
|
48
|
+
documentLayoutChanged: boolean;
|
|
49
|
+
requiresFullDecode: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type BlockRoomTransactionOriginKind = "local" | "remote";
|
|
53
|
+
|
|
54
|
+
/** Transaction metadata retained without exposing Y.Transaction to consumers. */
|
|
55
|
+
export interface ObservedBlockRoomChange {
|
|
56
|
+
changeSet: BlockRoomChangeSet;
|
|
57
|
+
origin: unknown;
|
|
58
|
+
originKind: BlockRoomTransactionOriginKind;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface BlockRoomAffectedBaseNodeSnapshot extends BlockRoomBaseNodeSnapshot {
|
|
62
|
+
pageSectionId?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CanonicalBlockRoomAffectedSnapshot {
|
|
66
|
+
baseNodes: BlockRoomAffectedBaseNodeSnapshot[];
|
|
67
|
+
deletedBaseBlockIds: string[];
|
|
68
|
+
localeNodes: BlockRoomLocaleNodeSnapshot[];
|
|
69
|
+
deletedLocaleBlockIds: string[];
|
|
70
|
+
localeValueTargets: AIDocumentFieldTarget[];
|
|
71
|
+
deletedLocaleValueTargets: AIDocumentFieldTarget[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
type TrackedRoomType =
|
|
75
|
+
| { scope: "root" }
|
|
76
|
+
| { scope: "base_nodes" }
|
|
77
|
+
| { scope: "base_node"; blockId: string }
|
|
78
|
+
| { scope: "base_order" }
|
|
79
|
+
| { scope: "order"; key: string }
|
|
80
|
+
| { scope: "locale_nodes" }
|
|
81
|
+
| { scope: "locale_node"; blockId: string }
|
|
82
|
+
| { scope: "locale_presence" };
|
|
83
|
+
|
|
84
|
+
interface BlockRoomTypeIndex {
|
|
85
|
+
descriptors: WeakMap<object, TrackedRoomType>;
|
|
86
|
+
orderIdsByKey: Map<string, string[]>;
|
|
87
|
+
localeValuesByKey: Map<
|
|
88
|
+
string,
|
|
89
|
+
{ target: AIDocumentFieldTarget; value: string }
|
|
90
|
+
>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function indexTrackedValue(
|
|
94
|
+
value: unknown,
|
|
95
|
+
descriptor: TrackedRoomType,
|
|
96
|
+
descriptors: WeakMap<object, TrackedRoomType>,
|
|
97
|
+
): void {
|
|
98
|
+
if (!(
|
|
99
|
+
value instanceof Y.Map ||
|
|
100
|
+
value instanceof Y.Array ||
|
|
101
|
+
value instanceof Y.Text
|
|
102
|
+
))
|
|
103
|
+
return;
|
|
104
|
+
descriptors.set(value, descriptor);
|
|
105
|
+
if (value instanceof Y.Map) {
|
|
106
|
+
for (const child of value.values())
|
|
107
|
+
indexTrackedValue(child, descriptor, descriptors);
|
|
108
|
+
} else if (value instanceof Y.Array) {
|
|
109
|
+
for (const child of value.toArray())
|
|
110
|
+
indexTrackedValue(child, descriptor, descriptors);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function blockRoomTypeIndex(yDocument: Y.Doc): BlockRoomTypeIndex {
|
|
115
|
+
const descriptors = new WeakMap<object, TrackedRoomType>();
|
|
116
|
+
const orderIdsByKey = new Map<string, string[]>();
|
|
117
|
+
const localeValuesByKey = new Map<
|
|
118
|
+
string,
|
|
119
|
+
{ target: AIDocumentFieldTarget; value: string }
|
|
120
|
+
>();
|
|
121
|
+
const root = yDocument.getMap<unknown>("block-document");
|
|
122
|
+
descriptors.set(root, { scope: "root" });
|
|
123
|
+
const baseNodes = blockRoomBaseNodes(yDocument);
|
|
124
|
+
descriptors.set(baseNodes, { scope: "base_nodes" });
|
|
125
|
+
for (const [blockId, node] of baseNodes.entries()) {
|
|
126
|
+
indexTrackedValue(node, { scope: "base_node", blockId }, descriptors);
|
|
127
|
+
}
|
|
128
|
+
const baseOrder = blockRoomBaseOrder(yDocument);
|
|
129
|
+
descriptors.set(baseOrder, { scope: "base_order" });
|
|
130
|
+
for (const [key, rawOrder] of baseOrder.entries()) {
|
|
131
|
+
const order =
|
|
132
|
+
rawOrder instanceof Y.Array
|
|
133
|
+
? rawOrder
|
|
134
|
+
: fail(`base_order:${key}:not_array`);
|
|
135
|
+
const ids = order
|
|
136
|
+
.toArray()
|
|
137
|
+
.map((id) => stringValue(id, `base_order:${key}:id`));
|
|
138
|
+
orderIdsByKey.set(key, ids);
|
|
139
|
+
descriptors.set(order, { scope: "order", key });
|
|
140
|
+
}
|
|
141
|
+
const localeNodes = blockRoomLocaleOverlay(yDocument);
|
|
142
|
+
descriptors.set(localeNodes, { scope: "locale_nodes" });
|
|
143
|
+
for (const [blockId, node] of localeNodes.entries()) {
|
|
144
|
+
indexTrackedValue(node, { scope: "locale_node", blockId }, descriptors);
|
|
145
|
+
}
|
|
146
|
+
descriptors.set(blockRoomLocalePresence(yDocument), {
|
|
147
|
+
scope: "locale_presence",
|
|
148
|
+
});
|
|
149
|
+
for (const target of blockRoomPresentLocaleValues(yDocument)) {
|
|
150
|
+
const key = canonicalBlockRoomLocaleValueTargetKey(yDocument, target);
|
|
151
|
+
localeValuesByKey.set(key, {
|
|
152
|
+
target,
|
|
153
|
+
value: JSON.stringify(blockRoomLocaleValue(yDocument, target)),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return { descriptors, orderIdsByKey, localeValuesByKey };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function changedIds(
|
|
160
|
+
previous: readonly string[],
|
|
161
|
+
current: readonly string[],
|
|
162
|
+
): string[] {
|
|
163
|
+
const previousPositions = new Map(previous.map((id, index) => [id, index]));
|
|
164
|
+
const currentPositions = new Map(current.map((id, index) => [id, index]));
|
|
165
|
+
return [...new Set([...previous, ...current])].filter(
|
|
166
|
+
(id) => previousPositions.get(id) !== currentPositions.get(id),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function collectRoomChangeSet(
|
|
171
|
+
transaction: Y.Transaction,
|
|
172
|
+
previous: BlockRoomTypeIndex,
|
|
173
|
+
current: BlockRoomTypeIndex,
|
|
174
|
+
): BlockRoomChangeSet {
|
|
175
|
+
const baseIds = new Set<string>();
|
|
176
|
+
const localeIds = new Set<string>();
|
|
177
|
+
const orderKeys = new Set<string>();
|
|
178
|
+
const localeValueTargets = new Map<string, AIDocumentFieldTarget>();
|
|
179
|
+
let documentMetadataChanged = false;
|
|
180
|
+
let documentLayoutChanged = false;
|
|
181
|
+
let requiresFullDecode = false;
|
|
182
|
+
const addOrder = (key: string) => {
|
|
183
|
+
orderKeys.add(key);
|
|
184
|
+
for (const id of changedIds(
|
|
185
|
+
previous.orderIdsByKey.get(key) ?? [],
|
|
186
|
+
current.orderIdsByKey.get(key) ?? [],
|
|
187
|
+
)) {
|
|
188
|
+
baseIds.add(id);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const mark = (
|
|
192
|
+
descriptor: TrackedRoomType,
|
|
193
|
+
keys: ReadonlySet<string | null> | undefined,
|
|
194
|
+
) => {
|
|
195
|
+
switch (descriptor.scope) {
|
|
196
|
+
case "base_node":
|
|
197
|
+
baseIds.add(descriptor.blockId);
|
|
198
|
+
return;
|
|
199
|
+
case "locale_node":
|
|
200
|
+
localeIds.add(descriptor.blockId);
|
|
201
|
+
return;
|
|
202
|
+
case "order":
|
|
203
|
+
addOrder(descriptor.key);
|
|
204
|
+
return;
|
|
205
|
+
case "base_nodes":
|
|
206
|
+
for (const key of keys ?? []) baseIds.add(key as string);
|
|
207
|
+
return;
|
|
208
|
+
case "base_order":
|
|
209
|
+
for (const key of keys ?? []) addOrder(key as string);
|
|
210
|
+
return;
|
|
211
|
+
case "locale_nodes":
|
|
212
|
+
for (const key of keys ?? []) localeIds.add(key as string);
|
|
213
|
+
return;
|
|
214
|
+
case "locale_presence":
|
|
215
|
+
return;
|
|
216
|
+
case "root":
|
|
217
|
+
for (const key of keys ?? []) {
|
|
218
|
+
if (key === "documentLayout") documentLayoutChanged = true;
|
|
219
|
+
else if (
|
|
220
|
+
key === "sourceLocale" ||
|
|
221
|
+
key === "roomLocale" ||
|
|
222
|
+
key === "profile" ||
|
|
223
|
+
key === "blockCatalogFingerprint"
|
|
224
|
+
) {
|
|
225
|
+
documentMetadataChanged = true;
|
|
226
|
+
} else requiresFullDecode = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
for (const [type, keys] of transaction.changed) {
|
|
231
|
+
const descriptor =
|
|
232
|
+
previous.descriptors.get(type) ?? current.descriptors.get(type);
|
|
233
|
+
if (descriptor) mark(descriptor, keys);
|
|
234
|
+
else requiresFullDecode = true;
|
|
235
|
+
}
|
|
236
|
+
for (const type of transaction.changedParentTypes.keys()) {
|
|
237
|
+
const descriptor =
|
|
238
|
+
previous.descriptors.get(type) ?? current.descriptors.get(type);
|
|
239
|
+
if (descriptor) mark(descriptor, undefined);
|
|
240
|
+
}
|
|
241
|
+
for (const key of new Set([
|
|
242
|
+
...previous.localeValuesByKey.keys(),
|
|
243
|
+
...current.localeValuesByKey.keys(),
|
|
244
|
+
])) {
|
|
245
|
+
const before = previous.localeValuesByKey.get(key);
|
|
246
|
+
const after = current.localeValuesByKey.get(key);
|
|
247
|
+
if (before?.value === after?.value) continue;
|
|
248
|
+
const target = after?.target ?? before!.target;
|
|
249
|
+
localeValueTargets.set(key, target);
|
|
250
|
+
if (target.owner.case === "blockHandle") localeIds.add(target.owner.value);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
affectedBaseBlockIds: [...baseIds].sort(),
|
|
254
|
+
affectedLocaleBlockIds: [...localeIds].sort(),
|
|
255
|
+
affectedLocaleValueTargets: [...localeValueTargets.entries()]
|
|
256
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
257
|
+
.map(([, target]) => target),
|
|
258
|
+
changedContainerOrderKeys: [...orderKeys].sort(),
|
|
259
|
+
documentMetadataChanged,
|
|
260
|
+
documentLayoutChanged,
|
|
261
|
+
requiresFullDecode,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function mergeBlockRoomChangeSets(
|
|
266
|
+
left: BlockRoomChangeSet | undefined,
|
|
267
|
+
right: BlockRoomChangeSet,
|
|
268
|
+
): BlockRoomChangeSet {
|
|
269
|
+
if (!left)
|
|
270
|
+
return {
|
|
271
|
+
...right,
|
|
272
|
+
affectedBaseBlockIds: [...right.affectedBaseBlockIds],
|
|
273
|
+
affectedLocaleBlockIds: [...right.affectedLocaleBlockIds],
|
|
274
|
+
affectedLocaleValueTargets: [...right.affectedLocaleValueTargets],
|
|
275
|
+
changedContainerOrderKeys: [...right.changedContainerOrderKeys],
|
|
276
|
+
};
|
|
277
|
+
return {
|
|
278
|
+
affectedBaseBlockIds: [
|
|
279
|
+
...new Set([...left.affectedBaseBlockIds, ...right.affectedBaseBlockIds]),
|
|
280
|
+
].sort(),
|
|
281
|
+
affectedLocaleBlockIds: [
|
|
282
|
+
...new Set([
|
|
283
|
+
...left.affectedLocaleBlockIds,
|
|
284
|
+
...right.affectedLocaleBlockIds,
|
|
285
|
+
]),
|
|
286
|
+
].sort(),
|
|
287
|
+
affectedLocaleValueTargets: [
|
|
288
|
+
...new Map(
|
|
289
|
+
[
|
|
290
|
+
...left.affectedLocaleValueTargets,
|
|
291
|
+
...right.affectedLocaleValueTargets,
|
|
292
|
+
].map(
|
|
293
|
+
(target) =>
|
|
294
|
+
[blockRoomLocaleValueTargetIdentity(target), target] as const,
|
|
295
|
+
),
|
|
296
|
+
).values(),
|
|
297
|
+
].sort((left, right) =>
|
|
298
|
+
blockRoomLocaleValueTargetIdentity(left).localeCompare(
|
|
299
|
+
blockRoomLocaleValueTargetIdentity(right),
|
|
300
|
+
),
|
|
301
|
+
),
|
|
302
|
+
changedContainerOrderKeys: [
|
|
303
|
+
...new Set([
|
|
304
|
+
...left.changedContainerOrderKeys,
|
|
305
|
+
...right.changedContainerOrderKeys,
|
|
306
|
+
]),
|
|
307
|
+
].sort(),
|
|
308
|
+
documentMetadataChanged:
|
|
309
|
+
left.documentMetadataChanged || right.documentMetadataChanged,
|
|
310
|
+
documentLayoutChanged:
|
|
311
|
+
left.documentLayoutChanged || right.documentLayoutChanged,
|
|
312
|
+
requiresFullDecode: left.requiresFullDecode || right.requiresFullDecode,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function observeBlockRoomChanges(
|
|
317
|
+
yDocument: Y.Doc,
|
|
318
|
+
listener: (change: ObservedBlockRoomChange) => void,
|
|
319
|
+
): () => void {
|
|
320
|
+
let index = blockRoomTypeIndex(yDocument);
|
|
321
|
+
const handler = (transaction: Y.Transaction): void => {
|
|
322
|
+
const next = blockRoomTypeIndex(yDocument);
|
|
323
|
+
const changeSet = collectRoomChangeSet(transaction, index, next);
|
|
324
|
+
index = next;
|
|
325
|
+
listener({
|
|
326
|
+
changeSet,
|
|
327
|
+
origin: transaction.origin,
|
|
328
|
+
originKind: transaction.local ? "local" : "remote",
|
|
329
|
+
});
|
|
330
|
+
};
|
|
331
|
+
yDocument.on("afterTransaction", handler);
|
|
332
|
+
return () => yDocument.off("afterTransaction", handler);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function decodeBaseNodeById(
|
|
336
|
+
yDocument: Y.Doc,
|
|
337
|
+
blockId: string,
|
|
338
|
+
): BlockRoomBaseNodeSnapshot | null {
|
|
339
|
+
const nodes = blockRoomBaseNodes(yDocument);
|
|
340
|
+
const rawNode = nodes.get(blockId);
|
|
341
|
+
if (rawNode === undefined) return null;
|
|
342
|
+
const reason = `base_node:${blockId}`;
|
|
343
|
+
const node = yMap(rawNode, reason);
|
|
344
|
+
const { family, kind, payload } = nodePayload(node, BASE_NODE_KEYS, reason);
|
|
345
|
+
const rawParentId = node.get("parentId");
|
|
346
|
+
const parentId =
|
|
347
|
+
rawParentId === null ? null : stringValue(rawParentId, `${reason}:parent`);
|
|
348
|
+
const containerSlot = stringValue(
|
|
349
|
+
node.get("containerSlot"),
|
|
350
|
+
`${reason}:slot`,
|
|
351
|
+
);
|
|
352
|
+
const key = orderContainerKey(parentId, containerSlot);
|
|
353
|
+
const rawOrder = blockRoomBaseOrder(yDocument).get(key);
|
|
354
|
+
const order =
|
|
355
|
+
rawOrder instanceof Y.Array
|
|
356
|
+
? rawOrder
|
|
357
|
+
: fail(`base_order:${key}:not_array`);
|
|
358
|
+
const ids = order
|
|
359
|
+
.toArray()
|
|
360
|
+
.map((id) => stringValue(id, `base_order:${key}:id`));
|
|
361
|
+
const position = ids.indexOf(blockId);
|
|
362
|
+
if (position < 0 || ids.lastIndexOf(blockId) !== position)
|
|
363
|
+
return fail(`base_order:${key}:invalid:${blockId}`);
|
|
364
|
+
const rawColumnId = node.get("columnId");
|
|
365
|
+
const columnId =
|
|
366
|
+
rawColumnId === undefined
|
|
367
|
+
? undefined
|
|
368
|
+
: stringValue(rawColumnId, `${reason}:column`);
|
|
369
|
+
return {
|
|
370
|
+
id: blockId,
|
|
371
|
+
family,
|
|
372
|
+
kind,
|
|
373
|
+
payload,
|
|
374
|
+
parentId,
|
|
375
|
+
containerSlot,
|
|
376
|
+
position,
|
|
377
|
+
...(columnId ? { columnId } : {}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function affectedPageSectionId(
|
|
382
|
+
yDocument: Y.Doc,
|
|
383
|
+
documentType: BlockRoomDocumentType,
|
|
384
|
+
node: BlockRoomBaseNodeSnapshot,
|
|
385
|
+
): string | undefined {
|
|
386
|
+
const visited = new Set([node.id]);
|
|
387
|
+
let parentId = node.parentId;
|
|
388
|
+
while (parentId !== null) {
|
|
389
|
+
if (visited.has(parentId)) fail(`base_node:${node.id}:parent_cycle`);
|
|
390
|
+
visited.add(parentId);
|
|
391
|
+
const parent = decodeBaseNodeById(yDocument, parentId);
|
|
392
|
+
if (!parent) fail(`base_node:${node.id}:missing_parent`);
|
|
393
|
+
if (parent.family === "page_section") {
|
|
394
|
+
if (documentType !== "page")
|
|
395
|
+
return fail(`base_node:${node.id}:parent_family`);
|
|
396
|
+
if (node.family === "rich_text") {
|
|
397
|
+
if (parent.kind !== "richText")
|
|
398
|
+
fail(`base_node:${node.id}:section_kind`);
|
|
399
|
+
return parent.id;
|
|
400
|
+
}
|
|
401
|
+
parentId = parent.parentId;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (parent.family !== node.family)
|
|
405
|
+
return fail(`base_node:${node.id}:parent_family`);
|
|
406
|
+
parentId = parent.parentId;
|
|
407
|
+
}
|
|
408
|
+
if (documentType === "page" && node.family === "rich_text") {
|
|
409
|
+
return fail(`base_node:${node.id}:missing_section_parent`);
|
|
410
|
+
}
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function decodeCanonicalBlockRoomAffectedNodes(
|
|
415
|
+
yDocument: Y.Doc,
|
|
416
|
+
documentType: BlockRoomDocumentType,
|
|
417
|
+
changeSet: BlockRoomChangeSet,
|
|
418
|
+
): CanonicalBlockRoomAffectedSnapshot {
|
|
419
|
+
if (changeSet.requiresFullDecode) fail("affected_nodes:full_decode_required");
|
|
420
|
+
if (roomDocumentType(yDocument) !== documentType) fail("document_type");
|
|
421
|
+
const requestedBaseIds = new Set(changeSet.affectedBaseBlockIds);
|
|
422
|
+
for (const blockId of changeSet.affectedLocaleBlockIds)
|
|
423
|
+
requestedBaseIds.add(blockId);
|
|
424
|
+
const baseNodes: BlockRoomAffectedBaseNodeSnapshot[] = [];
|
|
425
|
+
const deletedBaseBlockIds: string[] = [];
|
|
426
|
+
for (const blockId of [...requestedBaseIds].sort()) {
|
|
427
|
+
const node = decodeBaseNodeById(yDocument, blockId);
|
|
428
|
+
if (!node) {
|
|
429
|
+
if (changeSet.affectedBaseBlockIds.includes(blockId))
|
|
430
|
+
deletedBaseBlockIds.push(blockId);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const pageSectionId = affectedPageSectionId(yDocument, documentType, node);
|
|
434
|
+
baseNodes.push({ ...node, ...(pageSectionId ? { pageSectionId } : {}) });
|
|
435
|
+
}
|
|
436
|
+
const localeIds = new Set(changeSet.affectedLocaleBlockIds);
|
|
437
|
+
const presentKeys = new Set(
|
|
438
|
+
blockRoomPresentLocaleValues(yDocument).map((target) =>
|
|
439
|
+
blockRoomLocaleValueTargetIdentity(target),
|
|
440
|
+
),
|
|
441
|
+
);
|
|
442
|
+
const localeValueTargets = changeSet.affectedLocaleValueTargets.filter(
|
|
443
|
+
(target) => presentKeys.has(blockRoomLocaleValueTargetIdentity(target)),
|
|
444
|
+
);
|
|
445
|
+
const deletedLocaleValueTargets = changeSet.affectedLocaleValueTargets.filter(
|
|
446
|
+
(target) => !presentKeys.has(blockRoomLocaleValueTargetIdentity(target)),
|
|
447
|
+
);
|
|
448
|
+
const targetsByBlock = new Map<string, AIDocumentFieldTarget[]>();
|
|
449
|
+
for (const target of localeValueTargets) {
|
|
450
|
+
const blockId = blockRoomLocaleValueTargetBlockId(target);
|
|
451
|
+
targetsByBlock.set(
|
|
452
|
+
blockId,
|
|
453
|
+
localePresenceTargetsForBlock(yDocument, blockId),
|
|
454
|
+
);
|
|
455
|
+
localeIds.add(blockId);
|
|
456
|
+
}
|
|
457
|
+
const localeNodes: BlockRoomLocaleNodeSnapshot[] = [];
|
|
458
|
+
const deletedLocaleBlockIds: string[] = [];
|
|
459
|
+
for (const blockId of [...localeIds].sort()) {
|
|
460
|
+
const rawNode = blockRoomLocaleOverlay(yDocument).get(blockId);
|
|
461
|
+
if (rawNode === undefined) {
|
|
462
|
+
deletedLocaleBlockIds.push(blockId);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const reason = `locale:${blockId}`;
|
|
466
|
+
const decoded = nodePayload(
|
|
467
|
+
yMap(rawNode, reason),
|
|
468
|
+
LOCALE_NODE_KEYS,
|
|
469
|
+
reason,
|
|
470
|
+
);
|
|
471
|
+
const targets = targetsByBlock.get(blockId) ?? [];
|
|
472
|
+
if (targets.length === 0) {
|
|
473
|
+
if (!changeSet.affectedBaseBlockIds.includes(blockId))
|
|
474
|
+
fail(`affected_nodes:unmarked_locale_change:${blockId}`);
|
|
475
|
+
localeNodes.push({ id: blockId, ...decoded, payload: {} });
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
localeNodes.push({
|
|
479
|
+
id: blockId,
|
|
480
|
+
...decoded,
|
|
481
|
+
payload: sparseBlockRoomLocalePayload(yDocument, blockId, targets),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
baseNodes,
|
|
486
|
+
deletedBaseBlockIds,
|
|
487
|
+
localeNodes,
|
|
488
|
+
deletedLocaleBlockIds,
|
|
489
|
+
localeValueTargets,
|
|
490
|
+
deletedLocaleValueTargets,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function observeCanonicalBlockRoom(
|
|
495
|
+
yDocument: Y.Doc,
|
|
496
|
+
documentType: BlockRoomDocumentType,
|
|
497
|
+
listener: (change: CanonicalBlockRoomTransaction) => void,
|
|
498
|
+
): () => void {
|
|
499
|
+
const handler = (transaction: Y.Transaction): void => {
|
|
500
|
+
const snapshot = decodeCanonicalBlockRoom(yDocument, documentType);
|
|
501
|
+
listener({
|
|
502
|
+
document: snapshot.document,
|
|
503
|
+
snapshot,
|
|
504
|
+
origin: transaction.origin,
|
|
505
|
+
local: transaction.local,
|
|
506
|
+
});
|
|
507
|
+
};
|
|
508
|
+
yDocument.on("afterTransaction", handler);
|
|
509
|
+
return () => yDocument.off("afterTransaction", handler);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function transactBlockRoom(
|
|
513
|
+
yDocument: Y.Doc,
|
|
514
|
+
origin: unknown,
|
|
515
|
+
mutate: () => void,
|
|
516
|
+
): void {
|
|
517
|
+
yDocument.transact(mutate, origin);
|
|
518
|
+
}
|