@kyneta/yjs-schema 1.0.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 +21 -0
- package/README.md +182 -0
- package/dist/index.d.ts +351 -0
- package/dist/index.js +865 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/src/__tests__/bind-yjs.test.ts +266 -0
- package/src/__tests__/create.test.ts +632 -0
- package/src/__tests__/record-text-spike.test.ts +429 -0
- package/src/__tests__/store-reader.test.ts +722 -0
- package/src/__tests__/substrate.test.ts +604 -0
- package/src/__tests__/version.test.ts +227 -0
- package/src/bind-yjs.ts +147 -0
- package/src/change-mapping.ts +612 -0
- package/src/create.ts +172 -0
- package/src/index.ts +83 -0
- package/src/populate.ts +208 -0
- package/src/store-reader.ts +123 -0
- package/src/substrate.ts +252 -0
- package/src/sync.ts +107 -0
- package/src/version.ts +138 -0
- package/src/yjs-escape.ts +100 -0
- package/src/yjs-resolve.ts +108 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,865 @@
|
|
|
1
|
+
// src/create.ts
|
|
2
|
+
import { interpret, registerSubstrate } from "@kyneta/schema";
|
|
3
|
+
import { changefeed, readable, writable } from "@kyneta/schema";
|
|
4
|
+
|
|
5
|
+
// src/substrate.ts
|
|
6
|
+
import { buildWritableContext, executeBatch } from "@kyneta/schema";
|
|
7
|
+
import * as Y5 from "yjs";
|
|
8
|
+
|
|
9
|
+
// src/change-mapping.ts
|
|
10
|
+
import { advanceSchema as advanceSchema2, expandMapOpsToLeaves } from "@kyneta/schema";
|
|
11
|
+
import { RawPath } from "@kyneta/schema";
|
|
12
|
+
import * as Y2 from "yjs";
|
|
13
|
+
|
|
14
|
+
// src/yjs-resolve.ts
|
|
15
|
+
import { advanceSchema } from "@kyneta/schema";
|
|
16
|
+
import * as Y from "yjs";
|
|
17
|
+
function stepIntoYjs(current, segment) {
|
|
18
|
+
const resolved = segment.resolve();
|
|
19
|
+
if (current instanceof Y.Map) {
|
|
20
|
+
return current.get(resolved);
|
|
21
|
+
}
|
|
22
|
+
if (current instanceof Y.Array) {
|
|
23
|
+
return current.get(resolved);
|
|
24
|
+
}
|
|
25
|
+
if (current instanceof Y.Text) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`yjs-resolve: cannot step into Y.Text`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
function resolveYjsType(rootMap, rootSchema, path) {
|
|
33
|
+
let current = rootMap;
|
|
34
|
+
let schema = rootSchema;
|
|
35
|
+
let rootProduct = rootSchema;
|
|
36
|
+
while (rootProduct._kind === "annotated" && rootProduct.schema !== void 0) {
|
|
37
|
+
rootProduct = rootProduct.schema;
|
|
38
|
+
}
|
|
39
|
+
for (let i = 0; i < path.length; i++) {
|
|
40
|
+
const seg = path.segments[i];
|
|
41
|
+
const nextSchema = advanceSchema(schema, seg);
|
|
42
|
+
current = stepIntoYjs(current, seg);
|
|
43
|
+
schema = nextSchema;
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/change-mapping.ts
|
|
49
|
+
function applyChangeToYjs(rootMap, rootSchema, path, change2) {
|
|
50
|
+
switch (change2.type) {
|
|
51
|
+
case "text":
|
|
52
|
+
applyTextChange(rootMap, rootSchema, path, change2);
|
|
53
|
+
return;
|
|
54
|
+
case "sequence":
|
|
55
|
+
applySequenceChange(rootMap, rootSchema, path, change2);
|
|
56
|
+
return;
|
|
57
|
+
case "map":
|
|
58
|
+
applyMapChange(rootMap, rootSchema, path, change2);
|
|
59
|
+
return;
|
|
60
|
+
case "replace":
|
|
61
|
+
applyReplaceChange(rootMap, rootSchema, path, change2);
|
|
62
|
+
return;
|
|
63
|
+
case "increment":
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Yjs substrate does not support counter annotations. Use Schema.number() with ReplaceChange instead. Attempted IncrementChange with amount=${change2.amount} at path [${pathToString(path)}].`
|
|
66
|
+
);
|
|
67
|
+
case "tree":
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Yjs substrate does not support tree annotations. Yjs has no native tree type. Attempted TreeChange at path [${pathToString(path)}].`
|
|
70
|
+
);
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(
|
|
73
|
+
`applyChangeToYjs: unsupported change type "${change2.type}"`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function applyTextChange(rootMap, rootSchema, path, change2) {
|
|
78
|
+
const resolved = resolveYjsType(rootMap, rootSchema, path);
|
|
79
|
+
if (!(resolved instanceof Y2.Text)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`applyChangeToYjs: TextChange target at path [${pathToString(path)}] is not a Y.Text`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
resolved.applyDelta(change2.instructions);
|
|
85
|
+
}
|
|
86
|
+
function applySequenceChange(rootMap, rootSchema, path, change2) {
|
|
87
|
+
const resolved = resolveYjsType(rootMap, rootSchema, path);
|
|
88
|
+
if (!(resolved instanceof Y2.Array)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`applyChangeToYjs: SequenceChange target at path [${pathToString(path)}] is not a Y.Array`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const targetSchema = resolveSchemaAtPath(rootSchema, path);
|
|
94
|
+
const itemSchema = getItemSchema(targetSchema);
|
|
95
|
+
let cursor = 0;
|
|
96
|
+
for (const instruction of change2.instructions) {
|
|
97
|
+
if ("retain" in instruction) {
|
|
98
|
+
cursor += instruction.retain;
|
|
99
|
+
} else if ("delete" in instruction) {
|
|
100
|
+
resolved.delete(cursor, instruction.delete);
|
|
101
|
+
} else if ("insert" in instruction) {
|
|
102
|
+
const items = instruction.insert;
|
|
103
|
+
const yjsItems = items.map(
|
|
104
|
+
(item) => maybeCreateSharedType(item, itemSchema)
|
|
105
|
+
);
|
|
106
|
+
resolved.insert(cursor, yjsItems);
|
|
107
|
+
cursor += items.length;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function applyMapChange(rootMap, rootSchema, path, change2) {
|
|
112
|
+
const resolved = resolveYjsType(rootMap, rootSchema, path);
|
|
113
|
+
if (!(resolved instanceof Y2.Map)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`applyChangeToYjs: MapChange target at path [${pathToString(path)}] is not a Y.Map`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const targetSchema = resolveSchemaAtPath(rootSchema, path);
|
|
119
|
+
if (change2.delete) {
|
|
120
|
+
for (const key of change2.delete) {
|
|
121
|
+
resolved.delete(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (change2.set) {
|
|
125
|
+
for (const [key, value] of Object.entries(change2.set)) {
|
|
126
|
+
const fieldSchema = getFieldSchema(targetSchema, key);
|
|
127
|
+
const yjsValue = maybeCreateSharedType(value, fieldSchema);
|
|
128
|
+
resolved.set(key, yjsValue);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function applyReplaceChange(rootMap, rootSchema, path, change2) {
|
|
133
|
+
if (path.length === 0) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"applyChangeToYjs: ReplaceChange at root path is not supported"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
const lastSeg = path.segments[path.segments.length - 1];
|
|
139
|
+
const parentPath = path.slice(0, -1);
|
|
140
|
+
const parent = resolveYjsType(rootMap, rootSchema, parentPath);
|
|
141
|
+
const resolved = lastSeg.resolve();
|
|
142
|
+
if (parent instanceof Y2.Map && lastSeg.role === "key") {
|
|
143
|
+
const targetSchema = resolveSchemaAtPath(rootSchema, path);
|
|
144
|
+
const yjsValue = maybeCreateSharedType(change2.value, targetSchema);
|
|
145
|
+
parent.set(resolved, yjsValue);
|
|
146
|
+
} else if (parent instanceof Y2.Array && lastSeg.role === "index") {
|
|
147
|
+
const targetSchema = resolveSchemaAtPath(rootSchema, path);
|
|
148
|
+
const yjsValue = maybeCreateSharedType(change2.value, targetSchema);
|
|
149
|
+
parent.delete(resolved, 1);
|
|
150
|
+
parent.insert(resolved, [yjsValue]);
|
|
151
|
+
} else {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`applyChangeToYjs: ReplaceChange parent at path [${pathToString(parentPath)}] is not a Y.Map or Y.Array (got ${typeof parent})`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function maybeCreateSharedType(value, schema) {
|
|
158
|
+
if (schema === void 0) return value;
|
|
159
|
+
const structural = unwrapAnnotations(schema);
|
|
160
|
+
const tag = schema._kind === "annotated" ? schema.tag : void 0;
|
|
161
|
+
if (tag === "text") {
|
|
162
|
+
const text2 = new Y2.Text();
|
|
163
|
+
if (typeof value === "string" && value.length > 0) {
|
|
164
|
+
text2.insert(0, value);
|
|
165
|
+
}
|
|
166
|
+
return text2;
|
|
167
|
+
}
|
|
168
|
+
if (tag === "counter" || tag === "movable" || tag === "tree") {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Yjs substrate does not support "${tag}" annotations.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
switch (structural._kind) {
|
|
174
|
+
case "product": {
|
|
175
|
+
if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) {
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
return createStructuredMap(
|
|
179
|
+
value,
|
|
180
|
+
structural
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
case "sequence": {
|
|
184
|
+
if (!Array.isArray(value)) return value;
|
|
185
|
+
const arr = new Y2.Array();
|
|
186
|
+
const itemSchema = structural.item;
|
|
187
|
+
const items = value.map(
|
|
188
|
+
(item) => maybeCreateSharedType(item, itemSchema)
|
|
189
|
+
);
|
|
190
|
+
arr.insert(0, items);
|
|
191
|
+
return arr;
|
|
192
|
+
}
|
|
193
|
+
case "map": {
|
|
194
|
+
if (value === null || value === void 0 || typeof value !== "object" || Array.isArray(value)) {
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
const map = new Y2.Map();
|
|
198
|
+
const valueSchema = structural.item;
|
|
199
|
+
for (const [k, v] of Object.entries(
|
|
200
|
+
value
|
|
201
|
+
)) {
|
|
202
|
+
map.set(k, maybeCreateSharedType(v, valueSchema));
|
|
203
|
+
}
|
|
204
|
+
return map;
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function createStructuredMap(obj, productSchema) {
|
|
211
|
+
const map = new Y2.Map();
|
|
212
|
+
const structural = unwrapAnnotations(productSchema);
|
|
213
|
+
if (structural._kind !== "product") {
|
|
214
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
215
|
+
map.set(key, val);
|
|
216
|
+
}
|
|
217
|
+
return map;
|
|
218
|
+
}
|
|
219
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
220
|
+
if (val === void 0) continue;
|
|
221
|
+
const fieldSchema = structural.fields[key];
|
|
222
|
+
const yjsVal = fieldSchema ? maybeCreateSharedType(val, fieldSchema) : val;
|
|
223
|
+
map.set(key, yjsVal);
|
|
224
|
+
}
|
|
225
|
+
for (const [key, fieldSchema] of Object.entries(
|
|
226
|
+
structural.fields
|
|
227
|
+
)) {
|
|
228
|
+
if (key in obj) continue;
|
|
229
|
+
const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : void 0;
|
|
230
|
+
if (tag === "text") {
|
|
231
|
+
map.set(key, new Y2.Text());
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return map;
|
|
235
|
+
}
|
|
236
|
+
function eventsToOps(events) {
|
|
237
|
+
const ops = [];
|
|
238
|
+
for (const event of events) {
|
|
239
|
+
const kynetaPath = yjsPathToKynetaPath(event.path);
|
|
240
|
+
const change2 = eventToChange(event);
|
|
241
|
+
if (change2) {
|
|
242
|
+
ops.push({ path: kynetaPath, change: change2 });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return expandMapOpsToLeaves(ops);
|
|
246
|
+
}
|
|
247
|
+
function yjsPathToKynetaPath(yjsPath) {
|
|
248
|
+
let path = RawPath.empty;
|
|
249
|
+
for (const segment of yjsPath) {
|
|
250
|
+
if (typeof segment === "string") {
|
|
251
|
+
path = path.field(segment);
|
|
252
|
+
} else if (typeof segment === "number") {
|
|
253
|
+
path = path.item(segment);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return path;
|
|
257
|
+
}
|
|
258
|
+
function eventToChange(event) {
|
|
259
|
+
if (event.target instanceof Y2.Text) {
|
|
260
|
+
return textEventToChange(event);
|
|
261
|
+
}
|
|
262
|
+
if (event.target instanceof Y2.Array) {
|
|
263
|
+
return arrayEventToChange(event);
|
|
264
|
+
}
|
|
265
|
+
if (event.target instanceof Y2.Map) {
|
|
266
|
+
return mapEventToChange(event);
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
function textEventToChange(event) {
|
|
271
|
+
const instructions = [];
|
|
272
|
+
for (const delta of event.delta) {
|
|
273
|
+
if (delta.retain !== void 0) {
|
|
274
|
+
instructions.push({ retain: delta.retain });
|
|
275
|
+
} else if (delta.insert !== void 0) {
|
|
276
|
+
instructions.push({ insert: delta.insert });
|
|
277
|
+
} else if (delta.delete !== void 0) {
|
|
278
|
+
instructions.push({ delete: delta.delete });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { type: "text", instructions };
|
|
282
|
+
}
|
|
283
|
+
function arrayEventToChange(event) {
|
|
284
|
+
const instructions = [];
|
|
285
|
+
for (const delta of event.changes.delta) {
|
|
286
|
+
if (delta.retain !== void 0) {
|
|
287
|
+
instructions.push({ retain: delta.retain });
|
|
288
|
+
} else if (delta.delete !== void 0) {
|
|
289
|
+
instructions.push({ delete: delta.delete });
|
|
290
|
+
} else if (delta.insert !== void 0) {
|
|
291
|
+
const items = delta.insert.map(
|
|
292
|
+
(item) => extractEventValue(item)
|
|
293
|
+
);
|
|
294
|
+
instructions.push({ insert: items });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return { type: "sequence", instructions };
|
|
298
|
+
}
|
|
299
|
+
function mapEventToChange(event) {
|
|
300
|
+
const set = {};
|
|
301
|
+
const deleteKeys = [];
|
|
302
|
+
let hasSet = false;
|
|
303
|
+
let hasDelete = false;
|
|
304
|
+
const target = event.target;
|
|
305
|
+
event.changes.keys.forEach(
|
|
306
|
+
(change2, key) => {
|
|
307
|
+
if (change2.action === "add" || change2.action === "update") {
|
|
308
|
+
const value = target.get(key);
|
|
309
|
+
set[key] = extractEventValue(value);
|
|
310
|
+
hasSet = true;
|
|
311
|
+
} else if (change2.action === "delete") {
|
|
312
|
+
deleteKeys.push(key);
|
|
313
|
+
hasDelete = true;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
if (!hasSet && !hasDelete) return null;
|
|
318
|
+
return {
|
|
319
|
+
type: "map",
|
|
320
|
+
...hasSet ? { set } : {},
|
|
321
|
+
...hasDelete ? { delete: deleteKeys } : {}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function extractEventValue(value) {
|
|
325
|
+
if (value instanceof Y2.Map) return value.toJSON();
|
|
326
|
+
if (value instanceof Y2.Array) return value.toJSON();
|
|
327
|
+
if (value instanceof Y2.Text) return value.toJSON();
|
|
328
|
+
return value;
|
|
329
|
+
}
|
|
330
|
+
function unwrapAnnotations(schema) {
|
|
331
|
+
let s = schema;
|
|
332
|
+
while (s._kind === "annotated" && s.schema !== void 0) {
|
|
333
|
+
s = s.schema;
|
|
334
|
+
}
|
|
335
|
+
return s;
|
|
336
|
+
}
|
|
337
|
+
function resolveSchemaAtPath(rootSchema, path) {
|
|
338
|
+
let schema = rootSchema;
|
|
339
|
+
for (const seg of path.segments) {
|
|
340
|
+
schema = advanceSchema2(schema, seg);
|
|
341
|
+
}
|
|
342
|
+
return schema;
|
|
343
|
+
}
|
|
344
|
+
function getItemSchema(schema) {
|
|
345
|
+
const structural = unwrapAnnotations(schema);
|
|
346
|
+
return structural._kind === "sequence" ? structural.item : void 0;
|
|
347
|
+
}
|
|
348
|
+
function getFieldSchema(schema, key) {
|
|
349
|
+
const structural = unwrapAnnotations(schema);
|
|
350
|
+
if (structural._kind === "product") {
|
|
351
|
+
return structural.fields[key];
|
|
352
|
+
}
|
|
353
|
+
if (structural._kind === "map") {
|
|
354
|
+
return structural.item;
|
|
355
|
+
}
|
|
356
|
+
return void 0;
|
|
357
|
+
}
|
|
358
|
+
function pathToString(path) {
|
|
359
|
+
return path.segments.map((seg) => String(seg.resolve())).join(".");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/populate.ts
|
|
363
|
+
import { Zero } from "@kyneta/schema";
|
|
364
|
+
import * as Y3 from "yjs";
|
|
365
|
+
function ensureContainers(doc, schema) {
|
|
366
|
+
const rootMap = doc.getMap("root");
|
|
367
|
+
let rootProduct = schema;
|
|
368
|
+
while (rootProduct._kind === "annotated" && rootProduct.schema !== void 0) {
|
|
369
|
+
rootProduct = rootProduct.schema;
|
|
370
|
+
}
|
|
371
|
+
if (rootProduct._kind !== "product") {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
doc.transact(() => {
|
|
375
|
+
for (const [key, fieldSchema] of Object.entries(rootProduct.fields)) {
|
|
376
|
+
ensureRootField(rootMap, key, fieldSchema);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
function ensureRootField(rootMap, key, fieldSchema) {
|
|
381
|
+
const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : void 0;
|
|
382
|
+
switch (tag) {
|
|
383
|
+
case "text":
|
|
384
|
+
rootMap.set(key, new Y3.Text());
|
|
385
|
+
return;
|
|
386
|
+
case "counter":
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Yjs substrate does not support counter annotations. Use Schema.number() with ReplaceChange instead. Encountered counter annotation at root field "${key}".`
|
|
389
|
+
);
|
|
390
|
+
case "movable":
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Yjs substrate does not support movable list annotations. Yjs has no native movable list type. Encountered movable annotation at root field "${key}".`
|
|
393
|
+
);
|
|
394
|
+
case "tree":
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Yjs substrate does not support tree annotations. Yjs has no native tree type. Encountered tree annotation at root field "${key}".`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
const structural = unwrapAnnotations2(fieldSchema);
|
|
400
|
+
switch (structural._kind) {
|
|
401
|
+
case "product":
|
|
402
|
+
rootMap.set(key, ensureMapContainers(structural));
|
|
403
|
+
return;
|
|
404
|
+
case "sequence":
|
|
405
|
+
rootMap.set(key, new Y3.Array());
|
|
406
|
+
return;
|
|
407
|
+
case "map":
|
|
408
|
+
rootMap.set(key, new Y3.Map());
|
|
409
|
+
return;
|
|
410
|
+
case "scalar":
|
|
411
|
+
case "sum": {
|
|
412
|
+
const zero = Zero.structural(fieldSchema);
|
|
413
|
+
if (zero !== void 0) {
|
|
414
|
+
rootMap.set(key, zero);
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function ensureMapContainers(schema) {
|
|
421
|
+
const map = new Y3.Map();
|
|
422
|
+
const structural = unwrapAnnotations2(schema);
|
|
423
|
+
if (structural._kind !== "product") return map;
|
|
424
|
+
for (const [key, fieldSchema] of Object.entries(
|
|
425
|
+
structural.fields
|
|
426
|
+
)) {
|
|
427
|
+
const tag = fieldSchema._kind === "annotated" ? fieldSchema.tag : void 0;
|
|
428
|
+
if (tag === "text") {
|
|
429
|
+
map.set(key, new Y3.Text());
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const fs = unwrapAnnotations2(fieldSchema);
|
|
433
|
+
switch (fs._kind) {
|
|
434
|
+
case "product":
|
|
435
|
+
map.set(key, ensureMapContainers(fieldSchema));
|
|
436
|
+
break;
|
|
437
|
+
case "sequence":
|
|
438
|
+
map.set(key, new Y3.Array());
|
|
439
|
+
break;
|
|
440
|
+
case "map":
|
|
441
|
+
map.set(key, new Y3.Map());
|
|
442
|
+
break;
|
|
443
|
+
case "scalar":
|
|
444
|
+
case "sum": {
|
|
445
|
+
const zero = Zero.structural(fieldSchema);
|
|
446
|
+
if (zero !== void 0) {
|
|
447
|
+
map.set(key, zero);
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return map;
|
|
454
|
+
}
|
|
455
|
+
function unwrapAnnotations2(schema) {
|
|
456
|
+
let s = schema;
|
|
457
|
+
while (s._kind === "annotated" && s.schema !== void 0) {
|
|
458
|
+
s = s.schema;
|
|
459
|
+
}
|
|
460
|
+
return s;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/store-reader.ts
|
|
464
|
+
import * as Y4 from "yjs";
|
|
465
|
+
function extractValue(resolved) {
|
|
466
|
+
if (resolved instanceof Y4.Text) {
|
|
467
|
+
return resolved.toJSON();
|
|
468
|
+
}
|
|
469
|
+
if (resolved instanceof Y4.Map) {
|
|
470
|
+
return resolved.toJSON();
|
|
471
|
+
}
|
|
472
|
+
if (resolved instanceof Y4.Array) {
|
|
473
|
+
return resolved.toJSON();
|
|
474
|
+
}
|
|
475
|
+
return resolved;
|
|
476
|
+
}
|
|
477
|
+
function yjsStoreReader(doc, schema) {
|
|
478
|
+
const rootMap = doc.getMap("root");
|
|
479
|
+
return {
|
|
480
|
+
read(path) {
|
|
481
|
+
if (path.length === 0) {
|
|
482
|
+
return rootMap.toJSON();
|
|
483
|
+
}
|
|
484
|
+
const resolved = resolveYjsType(rootMap, schema, path);
|
|
485
|
+
return extractValue(resolved);
|
|
486
|
+
},
|
|
487
|
+
arrayLength(path) {
|
|
488
|
+
const resolved = resolveYjsType(rootMap, schema, path);
|
|
489
|
+
if (resolved instanceof Y4.Array) {
|
|
490
|
+
return resolved.length;
|
|
491
|
+
}
|
|
492
|
+
if (Array.isArray(resolved)) {
|
|
493
|
+
return resolved.length;
|
|
494
|
+
}
|
|
495
|
+
return 0;
|
|
496
|
+
},
|
|
497
|
+
keys(path) {
|
|
498
|
+
const resolved = resolveYjsType(rootMap, schema, path);
|
|
499
|
+
if (resolved instanceof Y4.Map) {
|
|
500
|
+
return Array.from(resolved.keys());
|
|
501
|
+
}
|
|
502
|
+
if (resolved !== null && resolved !== void 0 && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
503
|
+
return Object.keys(resolved);
|
|
504
|
+
}
|
|
505
|
+
return [];
|
|
506
|
+
},
|
|
507
|
+
hasKey(path, key) {
|
|
508
|
+
const resolved = resolveYjsType(rootMap, schema, path);
|
|
509
|
+
if (resolved instanceof Y4.Map) {
|
|
510
|
+
return resolved.has(key);
|
|
511
|
+
}
|
|
512
|
+
if (resolved !== null && resolved !== void 0 && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
513
|
+
return key in resolved;
|
|
514
|
+
}
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/version.ts
|
|
521
|
+
import { decodeStateVector } from "yjs";
|
|
522
|
+
function uint8ArrayToBase64(bytes) {
|
|
523
|
+
let binary = "";
|
|
524
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
525
|
+
binary += String.fromCharCode(bytes[i]);
|
|
526
|
+
}
|
|
527
|
+
return btoa(binary);
|
|
528
|
+
}
|
|
529
|
+
function base64ToUint8Array(base64) {
|
|
530
|
+
const binary = atob(base64);
|
|
531
|
+
const bytes = new Uint8Array(binary.length);
|
|
532
|
+
for (let i = 0; i < binary.length; i++) {
|
|
533
|
+
bytes[i] = binary.charCodeAt(i);
|
|
534
|
+
}
|
|
535
|
+
return bytes;
|
|
536
|
+
}
|
|
537
|
+
var YjsVersion = class _YjsVersion {
|
|
538
|
+
sv;
|
|
539
|
+
constructor(sv) {
|
|
540
|
+
this.sv = sv;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Serialize the state vector to a base64 string.
|
|
544
|
+
*
|
|
545
|
+
* The encoding is: raw state vector bytes → base64.
|
|
546
|
+
* This is text-safe for embedding in HTML meta tags, URL parameters, etc.
|
|
547
|
+
*/
|
|
548
|
+
serialize() {
|
|
549
|
+
return uint8ArrayToBase64(this.sv);
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Compare with another version using version-vector partial order.
|
|
553
|
+
*
|
|
554
|
+
* Decodes both state vectors via `Y.decodeStateVector()` to get
|
|
555
|
+
* `Map<number, number>` (clientID → clock), then compares:
|
|
556
|
+
*
|
|
557
|
+
* - Collect the union of all client IDs from both maps.
|
|
558
|
+
* - For each client, compare clocks (missing client = clock 0).
|
|
559
|
+
* - If all clocks in `this` ≤ `other` and at least one strictly less → `"behind"`
|
|
560
|
+
* - If all clocks in `this` ≥ `other` and at least one strictly greater → `"ahead"`
|
|
561
|
+
* - If all clocks equal → `"equal"`
|
|
562
|
+
* - Otherwise → `"concurrent"`
|
|
563
|
+
*
|
|
564
|
+
* Throws if `other` is not a `YjsVersion`.
|
|
565
|
+
*/
|
|
566
|
+
compare(other) {
|
|
567
|
+
if (!(other instanceof _YjsVersion)) {
|
|
568
|
+
throw new Error(
|
|
569
|
+
"YjsVersion can only be compared with another YjsVersion"
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
const thisMap = decodeStateVector(this.sv);
|
|
573
|
+
const otherMap = decodeStateVector(other.sv);
|
|
574
|
+
const allClients = /* @__PURE__ */ new Set();
|
|
575
|
+
for (const id of thisMap.keys()) allClients.add(id);
|
|
576
|
+
for (const id of otherMap.keys()) allClients.add(id);
|
|
577
|
+
let hasLess = false;
|
|
578
|
+
let hasGreater = false;
|
|
579
|
+
for (const clientId of allClients) {
|
|
580
|
+
const thisClock = thisMap.get(clientId) ?? 0;
|
|
581
|
+
const otherClock = otherMap.get(clientId) ?? 0;
|
|
582
|
+
if (thisClock < otherClock) {
|
|
583
|
+
hasLess = true;
|
|
584
|
+
}
|
|
585
|
+
if (thisClock > otherClock) {
|
|
586
|
+
hasGreater = true;
|
|
587
|
+
}
|
|
588
|
+
if (hasLess && hasGreater) {
|
|
589
|
+
return "concurrent";
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (hasLess && !hasGreater) return "behind";
|
|
593
|
+
if (hasGreater && !hasLess) return "ahead";
|
|
594
|
+
return "equal";
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Parse a serialized YjsVersion string back into a YjsVersion.
|
|
598
|
+
*
|
|
599
|
+
* The inverse of `serialize()`: base64 → `Uint8Array`.
|
|
600
|
+
*/
|
|
601
|
+
static parse(serialized) {
|
|
602
|
+
if (serialized === "") {
|
|
603
|
+
throw new Error("Invalid YjsVersion value: (empty string)");
|
|
604
|
+
}
|
|
605
|
+
const bytes = base64ToUint8Array(serialized);
|
|
606
|
+
return new _YjsVersion(bytes);
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// src/yjs-escape.ts
|
|
611
|
+
import { unwrap } from "@kyneta/schema";
|
|
612
|
+
var substrateToYjsDoc = /* @__PURE__ */ new WeakMap();
|
|
613
|
+
function registerYjsSubstrate(substrate, doc) {
|
|
614
|
+
substrateToYjsDoc.set(substrate, doc);
|
|
615
|
+
}
|
|
616
|
+
function yjs(ref) {
|
|
617
|
+
let substrate;
|
|
618
|
+
try {
|
|
619
|
+
substrate = unwrap(ref);
|
|
620
|
+
} catch {
|
|
621
|
+
throw new Error(
|
|
622
|
+
"yjs() requires a ref backed by a Yjs substrate. Use a doc created by exchange.get() with a bindYjs() schema, or by createYjsDoc()."
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
const doc = substrateToYjsDoc.get(substrate);
|
|
626
|
+
if (!doc) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
"yjs() requires a ref backed by a Yjs substrate. The ref has a substrate but it is not a Yjs substrate. Use a doc created with a bindYjs() schema or createYjsDoc()."
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
return doc;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/substrate.ts
|
|
635
|
+
var KYNETA_ORIGIN = "kyneta-prepare";
|
|
636
|
+
function createYjsSubstrate(doc, schema) {
|
|
637
|
+
const pendingChanges = [];
|
|
638
|
+
let inOurTransaction = false;
|
|
639
|
+
let pendingImportOrigin;
|
|
640
|
+
let cachedCtx;
|
|
641
|
+
const rootMap = doc.getMap("root");
|
|
642
|
+
const reader = yjsStoreReader(doc, schema);
|
|
643
|
+
const substrate = {
|
|
644
|
+
store: reader,
|
|
645
|
+
prepare(path, change2) {
|
|
646
|
+
if (!inOurTransaction) {
|
|
647
|
+
pendingChanges.push({ path, change: change2 });
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
onFlush(origin) {
|
|
651
|
+
if (!inOurTransaction && pendingChanges.length > 0) {
|
|
652
|
+
inOurTransaction = true;
|
|
653
|
+
try {
|
|
654
|
+
doc.transact(() => {
|
|
655
|
+
for (const { path, change: change2 } of pendingChanges) {
|
|
656
|
+
applyChangeToYjs(rootMap, schema, path, change2);
|
|
657
|
+
}
|
|
658
|
+
}, KYNETA_ORIGIN);
|
|
659
|
+
pendingChanges.length = 0;
|
|
660
|
+
} finally {
|
|
661
|
+
inOurTransaction = false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
context() {
|
|
666
|
+
if (!cachedCtx) {
|
|
667
|
+
cachedCtx = buildWritableContext(substrate);
|
|
668
|
+
}
|
|
669
|
+
return cachedCtx;
|
|
670
|
+
},
|
|
671
|
+
version() {
|
|
672
|
+
return new YjsVersion(Y5.encodeStateVector(doc));
|
|
673
|
+
},
|
|
674
|
+
exportSnapshot() {
|
|
675
|
+
return {
|
|
676
|
+
encoding: "binary",
|
|
677
|
+
data: Y5.encodeStateAsUpdate(doc)
|
|
678
|
+
};
|
|
679
|
+
},
|
|
680
|
+
exportSince(since) {
|
|
681
|
+
try {
|
|
682
|
+
const bytes = Y5.encodeStateAsUpdate(doc, since.sv);
|
|
683
|
+
return { encoding: "binary", data: bytes };
|
|
684
|
+
} catch {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
importDelta(payload, origin) {
|
|
689
|
+
if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
|
|
690
|
+
throw new Error(
|
|
691
|
+
"YjsSubstrate.importDelta only supports binary-encoded payloads"
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
pendingImportOrigin = origin;
|
|
695
|
+
try {
|
|
696
|
+
Y5.applyUpdate(doc, payload.data, origin ?? "remote");
|
|
697
|
+
} finally {
|
|
698
|
+
pendingImportOrigin = void 0;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
rootMap.observeDeep((events, transaction) => {
|
|
703
|
+
if (transaction.origin === KYNETA_ORIGIN) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const ops = eventsToOps(events);
|
|
707
|
+
if (ops.length === 0) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const origin = pendingImportOrigin ?? (typeof transaction.origin === "string" ? transaction.origin : void 0);
|
|
711
|
+
const ctx = substrate.context();
|
|
712
|
+
inOurTransaction = true;
|
|
713
|
+
try {
|
|
714
|
+
executeBatch(ctx, ops, origin);
|
|
715
|
+
} finally {
|
|
716
|
+
inOurTransaction = false;
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
registerYjsSubstrate(substrate, doc);
|
|
720
|
+
return substrate;
|
|
721
|
+
}
|
|
722
|
+
var yjsSubstrateFactory = {
|
|
723
|
+
create(schema) {
|
|
724
|
+
const doc = new Y5.Doc();
|
|
725
|
+
ensureContainers(doc, schema);
|
|
726
|
+
return createYjsSubstrate(doc, schema);
|
|
727
|
+
},
|
|
728
|
+
fromSnapshot(payload, schema) {
|
|
729
|
+
if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
"YjsSubstrateFactory.fromSnapshot only supports binary-encoded payloads"
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
const doc = new Y5.Doc();
|
|
735
|
+
Y5.applyUpdate(doc, payload.data);
|
|
736
|
+
return createYjsSubstrate(doc, schema);
|
|
737
|
+
},
|
|
738
|
+
parseVersion(serialized) {
|
|
739
|
+
return YjsVersion.parse(serialized);
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
// src/create.ts
|
|
744
|
+
var substrates = /* @__PURE__ */ new WeakMap();
|
|
745
|
+
function getSubstrate(doc) {
|
|
746
|
+
const s = substrates.get(doc);
|
|
747
|
+
if (!s) {
|
|
748
|
+
throw new Error(
|
|
749
|
+
"version/exportSnapshot/importDelta called on an object without a YjsSubstrate. Use a doc created by createYjsDoc() or createYjsDocFromSnapshot()."
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
return s;
|
|
753
|
+
}
|
|
754
|
+
function registerDoc(schema, substrate) {
|
|
755
|
+
const doc = interpret(schema, substrate.context()).with(readable).with(writable).with(changefeed).done();
|
|
756
|
+
substrates.set(doc, substrate);
|
|
757
|
+
registerSubstrate(doc, substrate);
|
|
758
|
+
return doc;
|
|
759
|
+
}
|
|
760
|
+
function isYDoc(value) {
|
|
761
|
+
return value !== null && value !== void 0 && typeof value === "object" && "getMap" in value && "getText" in value && "getArray" in value && "transact" in value && typeof value.transact === "function" && // Y.Doc has clientID; distinguish from other objects
|
|
762
|
+
"clientID" in value && typeof value.clientID === "number";
|
|
763
|
+
}
|
|
764
|
+
var createYjsDoc = (schema, doc) => {
|
|
765
|
+
if (doc !== void 0 && isYDoc(doc)) {
|
|
766
|
+
return registerDoc(schema, createYjsSubstrate(doc, schema));
|
|
767
|
+
}
|
|
768
|
+
return registerDoc(schema, yjsSubstrateFactory.create(schema));
|
|
769
|
+
};
|
|
770
|
+
var createYjsDocFromSnapshot = (schema, payload) => registerDoc(schema, yjsSubstrateFactory.fromSnapshot(payload, schema));
|
|
771
|
+
|
|
772
|
+
// src/sync.ts
|
|
773
|
+
function version(doc) {
|
|
774
|
+
return getSubstrate(doc).version();
|
|
775
|
+
}
|
|
776
|
+
function exportSnapshot(doc) {
|
|
777
|
+
return getSubstrate(doc).exportSnapshot();
|
|
778
|
+
}
|
|
779
|
+
function exportSince(doc, since) {
|
|
780
|
+
return getSubstrate(doc).exportSince(since);
|
|
781
|
+
}
|
|
782
|
+
function importDelta(doc, payload, origin) {
|
|
783
|
+
getSubstrate(doc).importDelta(payload, origin);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/index.ts
|
|
787
|
+
import { applyChanges, change } from "@kyneta/schema";
|
|
788
|
+
import { subscribe, subscribeNode } from "@kyneta/schema";
|
|
789
|
+
import { Schema } from "@kyneta/schema";
|
|
790
|
+
import { Schema as Schema2 } from "@kyneta/schema";
|
|
791
|
+
|
|
792
|
+
// src/bind-yjs.ts
|
|
793
|
+
import { bind } from "@kyneta/schema";
|
|
794
|
+
import * as Y6 from "yjs";
|
|
795
|
+
function hashPeerId(peerId) {
|
|
796
|
+
let hash = 2166136261;
|
|
797
|
+
for (let i = 0; i < peerId.length; i++) {
|
|
798
|
+
hash ^= peerId.charCodeAt(i);
|
|
799
|
+
hash = Math.imul(hash, 16777619);
|
|
800
|
+
}
|
|
801
|
+
return hash >>> 0;
|
|
802
|
+
}
|
|
803
|
+
function createYjsFactory(peerId) {
|
|
804
|
+
const numericClientId = hashPeerId(peerId);
|
|
805
|
+
return {
|
|
806
|
+
create(schema) {
|
|
807
|
+
const doc = new Y6.Doc();
|
|
808
|
+
doc.clientID = numericClientId;
|
|
809
|
+
ensureContainers(doc, schema);
|
|
810
|
+
return createYjsSubstrate(doc, schema);
|
|
811
|
+
},
|
|
812
|
+
fromSnapshot(payload, schema) {
|
|
813
|
+
if (payload.encoding !== "binary" || !(payload.data instanceof Uint8Array)) {
|
|
814
|
+
throw new Error(
|
|
815
|
+
"YjsSubstrateFactory.fromSnapshot only supports binary-encoded payloads"
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
const doc = new Y6.Doc();
|
|
819
|
+
doc.clientID = numericClientId;
|
|
820
|
+
Y6.applyUpdate(doc, payload.data);
|
|
821
|
+
return createYjsSubstrate(doc, schema);
|
|
822
|
+
},
|
|
823
|
+
parseVersion(serialized) {
|
|
824
|
+
return YjsVersion.parse(serialized);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
function bindYjs(schema) {
|
|
829
|
+
return bind({
|
|
830
|
+
schema,
|
|
831
|
+
factory: (ctx) => createYjsFactory(ctx.peerId),
|
|
832
|
+
strategy: "causal"
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/index.ts
|
|
837
|
+
function text() {
|
|
838
|
+
return Schema2.annotated("text");
|
|
839
|
+
}
|
|
840
|
+
export {
|
|
841
|
+
Schema,
|
|
842
|
+
YjsVersion,
|
|
843
|
+
applyChangeToYjs,
|
|
844
|
+
applyChanges,
|
|
845
|
+
bindYjs,
|
|
846
|
+
change,
|
|
847
|
+
createYjsDoc,
|
|
848
|
+
createYjsDocFromSnapshot,
|
|
849
|
+
createYjsSubstrate,
|
|
850
|
+
ensureContainers,
|
|
851
|
+
eventsToOps,
|
|
852
|
+
exportSince,
|
|
853
|
+
exportSnapshot,
|
|
854
|
+
importDelta,
|
|
855
|
+
resolveYjsType,
|
|
856
|
+
stepIntoYjs,
|
|
857
|
+
subscribe,
|
|
858
|
+
subscribeNode,
|
|
859
|
+
text,
|
|
860
|
+
version,
|
|
861
|
+
yjs,
|
|
862
|
+
yjsStoreReader,
|
|
863
|
+
yjsSubstrateFactory
|
|
864
|
+
};
|
|
865
|
+
//# sourceMappingURL=index.js.map
|