@echovisionlab/geul-common 0.2.3 → 0.2.4

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.
@@ -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
+ }