@echovisionlab/geul-common 0.2.3 → 0.3.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/package.json +3 -2
- package/src/collaboration/block-room-codec/inline-content-model.ts +308 -0
- package/src/collaboration/block-room-codec/inline-content-mutations.ts +10 -296
- package/src/collaboration/block-room-codec/inline-content-projection.ts +4 -15
- package/src/collaboration/block-room-codec/internal.ts +39 -20
- package/src/collaboration/block-room-codec/locale-presence-storage.ts +170 -0
- package/src/collaboration/block-room-codec/locale-presence.ts +25 -839
- package/src/collaboration/block-room-codec/locale-targets.ts +513 -0
- package/src/collaboration/block-room-codec/locale-value-projection.ts +183 -0
- package/src/collaboration/block-room-codec/replay-base.ts +195 -0
- package/src/collaboration/block-room-codec/replay-locale.ts +182 -0
- package/src/collaboration/block-room-codec/replay-payload.ts +317 -0
- package/src/collaboration/block-room-codec/replay-structure.ts +504 -0
- package/src/collaboration/block-room-codec/replay.ts +75 -0
- package/src/collaboration/block-room-codec.ts +1 -0
- package/src/collaboration/form-schema-delta.ts +440 -0
- package/src/collaboration/menu.ts +109 -19
- package/src/index.ts +1 -0
|
@@ -354,24 +354,50 @@ function identityArray(
|
|
|
354
354
|
value: JsonValue,
|
|
355
355
|
collection: string,
|
|
356
356
|
identity: string,
|
|
357
|
-
nestedCollection?: string,
|
|
358
|
-
nestedIdentity?: string,
|
|
359
357
|
): unknown[] {
|
|
360
358
|
const object = jsonObject(value, "locale_identity:payload");
|
|
361
359
|
const children = object[collection];
|
|
362
360
|
if (!Array.isArray(children)) return [];
|
|
361
|
+
return children.map(
|
|
362
|
+
(child) => jsonObject(child, "locale_identity:item")[identity],
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const tableIdentityPattern =
|
|
367
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
368
|
+
|
|
369
|
+
function tableIdentityArray(
|
|
370
|
+
value: JsonValue,
|
|
371
|
+
collection: string,
|
|
372
|
+
identity: string,
|
|
373
|
+
nestedIdentity: string,
|
|
374
|
+
): unknown[] {
|
|
375
|
+
const object = jsonObject(value, "locale_identity:payload");
|
|
376
|
+
const children = object[collection];
|
|
377
|
+
if (!Array.isArray(children)) return [];
|
|
378
|
+
const seenRows = new Set<string>();
|
|
379
|
+
const seenCells = new Set<string>();
|
|
363
380
|
return children.map((child) => {
|
|
364
381
|
const item = jsonObject(child, "locale_identity:item");
|
|
365
|
-
|
|
366
|
-
|
|
382
|
+
const rowId = item[identity];
|
|
383
|
+
if (typeof rowId !== "string" || !tableIdentityPattern.test(rowId))
|
|
384
|
+
fail("locale_identity:table_row_id");
|
|
385
|
+
if (seenRows.has(rowId)) fail("locale_identity:table_duplicate_row_id");
|
|
386
|
+
seenRows.add(rowId);
|
|
387
|
+
const nested = item.cells;
|
|
388
|
+
const cells = Array.isArray(nested) ? nested : [];
|
|
367
389
|
return {
|
|
368
|
-
id:
|
|
369
|
-
children:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
)
|
|
374
|
-
|
|
390
|
+
id: rowId,
|
|
391
|
+
children: cells.map((entry) => {
|
|
392
|
+
const cell = jsonObject(entry, "locale_identity:nested");
|
|
393
|
+
const cellId = cell[nestedIdentity];
|
|
394
|
+
if (typeof cellId !== "string" || !tableIdentityPattern.test(cellId))
|
|
395
|
+
fail("locale_identity:table_cell_id");
|
|
396
|
+
if (seenCells.has(cellId))
|
|
397
|
+
fail("locale_identity:table_duplicate_cell_id");
|
|
398
|
+
seenCells.add(cellId);
|
|
399
|
+
return cellId;
|
|
400
|
+
}),
|
|
375
401
|
};
|
|
376
402
|
});
|
|
377
403
|
}
|
|
@@ -402,18 +428,11 @@ export function assertBlockRoomLocaleProjectionParity(yDocument: Y.Doc): void {
|
|
|
402
428
|
jsonObject(localePayload, `locale:${id}:payload`).content,
|
|
403
429
|
`locale:${id}:content`,
|
|
404
430
|
);
|
|
405
|
-
const baseIdentity =
|
|
406
|
-
|
|
407
|
-
"rows",
|
|
408
|
-
"id",
|
|
409
|
-
"cells",
|
|
410
|
-
"id",
|
|
411
|
-
);
|
|
412
|
-
const localeIdentity = identityArray(
|
|
431
|
+
const baseIdentity = tableIdentityArray(baseContent, "rows", "id", "id");
|
|
432
|
+
const localeIdentity = tableIdentityArray(
|
|
413
433
|
localeContent,
|
|
414
434
|
"rows",
|
|
415
435
|
"rowId",
|
|
416
|
-
"cells",
|
|
417
436
|
"cellId",
|
|
418
437
|
);
|
|
419
438
|
if (JSON.stringify(baseIdentity) !== JSON.stringify(localeIdentity))
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { AIDocumentFieldTarget } from "@echovisionlab/geul-proto/secure/ai_pb.ts";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
import { BLOCK_ROOM_LOCALE_PRESENCE } from "./internal.ts";
|
|
4
|
+
import {
|
|
5
|
+
allBlockRoomLocaleValueTargets,
|
|
6
|
+
canonicalBlockRoomLocaleValueTarget,
|
|
7
|
+
canonicalBlockRoomLocaleValueTargetKey,
|
|
8
|
+
canonicalTarget,
|
|
9
|
+
presenceFail,
|
|
10
|
+
targetFromParts,
|
|
11
|
+
type CanonicalPathSegment,
|
|
12
|
+
} from "./locale-targets.ts";
|
|
13
|
+
import { blockRoomLocaleValue } from "./locale-value-projection.ts";
|
|
14
|
+
|
|
15
|
+
export function blockRoomLocalePresence(yDocument: Y.Doc): Y.Map<unknown> {
|
|
16
|
+
const value = yDocument
|
|
17
|
+
.getMap<unknown>("block-document")
|
|
18
|
+
.get(BLOCK_ROOM_LOCALE_PRESENCE);
|
|
19
|
+
return value instanceof Y.Map ? value : presenceFail("map");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hydrateBlockRoomLocalePresence(
|
|
23
|
+
yDocument: Y.Doc,
|
|
24
|
+
values: readonly AIDocumentFieldTarget[],
|
|
25
|
+
): void {
|
|
26
|
+
const presence = blockRoomLocalePresence(yDocument);
|
|
27
|
+
let previous = "";
|
|
28
|
+
for (const value of values) {
|
|
29
|
+
const target = canonicalBlockRoomLocaleValueTarget(yDocument, value);
|
|
30
|
+
const key = canonicalTarget(target).key;
|
|
31
|
+
if (key <= previous) return presenceFail("order_or_duplicate");
|
|
32
|
+
previous = key;
|
|
33
|
+
presence.set(key, true);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function targetFromKey(key: string): AIDocumentFieldTarget {
|
|
38
|
+
let decoded: unknown;
|
|
39
|
+
try {
|
|
40
|
+
decoded = JSON.parse(key);
|
|
41
|
+
} catch {
|
|
42
|
+
return presenceFail("key");
|
|
43
|
+
}
|
|
44
|
+
if (
|
|
45
|
+
!Array.isArray(decoded) ||
|
|
46
|
+
decoded.length !== 3 ||
|
|
47
|
+
!Array.isArray(decoded[2])
|
|
48
|
+
)
|
|
49
|
+
return presenceFail("key");
|
|
50
|
+
const [blockId, field, rawPath] = decoded;
|
|
51
|
+
if (typeof blockId !== "string" || typeof field !== "string")
|
|
52
|
+
return presenceFail("key");
|
|
53
|
+
const path: CanonicalPathSegment[] = rawPath.map((segment) => {
|
|
54
|
+
if (
|
|
55
|
+
!Array.isArray(segment) ||
|
|
56
|
+
segment.length !== 2 ||
|
|
57
|
+
(segment[0] !== "field" && segment[0] !== "item") ||
|
|
58
|
+
typeof segment[1] !== "string"
|
|
59
|
+
)
|
|
60
|
+
return presenceFail("key");
|
|
61
|
+
return [segment[0], segment[1]];
|
|
62
|
+
});
|
|
63
|
+
return targetFromParts(blockId, field, path);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function blockRoomPresentLocaleValues(
|
|
67
|
+
yDocument: Y.Doc,
|
|
68
|
+
): AIDocumentFieldTarget[] {
|
|
69
|
+
return [...blockRoomLocalePresence(yDocument).entries()]
|
|
70
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
71
|
+
.map(([key, value]) => {
|
|
72
|
+
if (value !== true) return presenceFail("value");
|
|
73
|
+
const target = targetFromKey(key);
|
|
74
|
+
canonicalBlockRoomLocaleValueTarget(yDocument, target);
|
|
75
|
+
return target;
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function markBlockRoomLocaleValuePresent(
|
|
80
|
+
yDocument: Y.Doc,
|
|
81
|
+
value: AIDocumentFieldTarget | undefined,
|
|
82
|
+
): void {
|
|
83
|
+
const key = canonicalBlockRoomLocaleValueTargetKey(yDocument, value);
|
|
84
|
+
blockRoomLocalePresence(yDocument).set(key, true);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function markAllBlockRoomLocaleValuesPresentForBlock(
|
|
88
|
+
yDocument: Y.Doc,
|
|
89
|
+
blockId: string,
|
|
90
|
+
): void {
|
|
91
|
+
for (const target of allBlockRoomLocaleValueTargets(yDocument)) {
|
|
92
|
+
if (target.owner.case === "blockHandle" && target.owner.value === blockId)
|
|
93
|
+
markBlockRoomLocaleValuePresent(yDocument, target);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function deleteBlockRoomLocalePresenceForBlocks(
|
|
98
|
+
yDocument: Y.Doc,
|
|
99
|
+
blockIds: ReadonlySet<string>,
|
|
100
|
+
): void {
|
|
101
|
+
const presence = blockRoomLocalePresence(yDocument);
|
|
102
|
+
for (const key of [...presence.keys()]) {
|
|
103
|
+
const target = canonicalTarget(targetFromKey(key));
|
|
104
|
+
if (blockIds.has(target.blockId)) presence.delete(key);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function assertBlockRoomLocalePresenceNotRemoved(
|
|
109
|
+
before: Y.Doc,
|
|
110
|
+
after: Y.Doc,
|
|
111
|
+
): void {
|
|
112
|
+
const beforeKeys = new Set(
|
|
113
|
+
blockRoomPresentLocaleValues(before).map(
|
|
114
|
+
(target) => canonicalTarget(target).key,
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
const afterKeys = new Set(
|
|
118
|
+
blockRoomPresentLocaleValues(after).map(
|
|
119
|
+
(target) => canonicalTarget(target).key,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
for (const key of beforeKeys) {
|
|
123
|
+
if (afterKeys.has(key)) continue;
|
|
124
|
+
try {
|
|
125
|
+
canonicalBlockRoomLocaleValueTarget(after, targetFromKey(key));
|
|
126
|
+
} catch {
|
|
127
|
+
// Source structural deletion and kind replacement remove targets that no
|
|
128
|
+
// longer exist in the post-state catalog. Those stale markers must go.
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
return presenceFail("removed");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function assertBlockRoomLocaleValueChangesPresent(
|
|
136
|
+
before: Y.Doc,
|
|
137
|
+
after: Y.Doc,
|
|
138
|
+
): void {
|
|
139
|
+
const beforeTargets = new Map(
|
|
140
|
+
allBlockRoomLocaleValueTargets(before).map((target) => [
|
|
141
|
+
canonicalBlockRoomLocaleValueTargetKey(before, target),
|
|
142
|
+
target,
|
|
143
|
+
]),
|
|
144
|
+
);
|
|
145
|
+
const afterPresence = new Set(
|
|
146
|
+
blockRoomPresentLocaleValues(after).map((target) =>
|
|
147
|
+
canonicalBlockRoomLocaleValueTargetKey(after, target),
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
for (const target of allBlockRoomLocaleValueTargets(after)) {
|
|
151
|
+
const key = canonicalBlockRoomLocaleValueTargetKey(after, target);
|
|
152
|
+
const beforeTarget = beforeTargets.get(key);
|
|
153
|
+
const changed =
|
|
154
|
+
!beforeTarget ||
|
|
155
|
+
JSON.stringify(blockRoomLocaleValue(before, beforeTarget)) !==
|
|
156
|
+
JSON.stringify(blockRoomLocaleValue(after, target));
|
|
157
|
+
if (changed && !afterPresence.has(key))
|
|
158
|
+
return presenceFail("unmarked_change");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function localePresenceTargetsForBlock(
|
|
163
|
+
yDocument: Y.Doc,
|
|
164
|
+
blockId: string,
|
|
165
|
+
): AIDocumentFieldTarget[] {
|
|
166
|
+
return blockRoomPresentLocaleValues(yDocument).filter(
|
|
167
|
+
(target) =>
|
|
168
|
+
target.owner.case === "blockHandle" && target.owner.value === blockId,
|
|
169
|
+
);
|
|
170
|
+
}
|