@syncular/server-plugin-yjs 0.0.0 → 0.0.6-165

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @syncular/server-plugin-crdt-yjs
1
+ # @syncular/server-plugin-yjs
2
2
 
3
3
  Yjs-first server integration helpers for Syncular.
4
4
 
@@ -19,7 +19,7 @@ Implemented:
19
19
  ## Example
20
20
 
21
21
  ```ts
22
- import { createYjsServerModule } from '@syncular/server-plugin-crdt-yjs';
22
+ import { createYjsServerModule } from '@syncular/server-plugin-yjs';
23
23
 
24
24
  const yjs = createYjsServerModule({
25
25
  rules: [
@@ -0,0 +1,91 @@
1
+ import { type SyncServerPushPlugin } from '@syncular/server';
2
+ export declare const YJS_PAYLOAD_KEY = "__yjs";
3
+ export type YjsFieldKind = 'text' | 'xml-fragment' | 'prosemirror';
4
+ export interface YjsServerFieldRule {
5
+ table: string;
6
+ field: string;
7
+ /**
8
+ * Column that stores canonical serialized Yjs state.
9
+ * Example: "content_yjs_state"
10
+ */
11
+ stateColumn: string;
12
+ /**
13
+ * Container key inside the Yjs document. Defaults to `field`.
14
+ */
15
+ containerKey?: string;
16
+ /**
17
+ * Snapshot row id column. Defaults to `id`.
18
+ */
19
+ rowIdField?: string;
20
+ /**
21
+ * CRDT container type.
22
+ */
23
+ kind?: YjsFieldKind;
24
+ }
25
+ export interface YjsServerUpdateEnvelope {
26
+ updateId: string;
27
+ updateBase64: string;
28
+ }
29
+ export type YjsServerUpdateInput = YjsServerUpdateEnvelope | readonly YjsServerUpdateEnvelope[];
30
+ export interface YjsServerPayloadEnvelope {
31
+ [field: string]: YjsServerUpdateInput;
32
+ }
33
+ export interface BuildYjsTextUpdateArgs {
34
+ previousStateBase64?: string | Uint8Array | null;
35
+ nextText: string;
36
+ containerKey?: string;
37
+ updateId?: string;
38
+ }
39
+ export interface BuildYjsTextUpdateResult {
40
+ update: YjsServerUpdateEnvelope;
41
+ nextStateBase64: string;
42
+ nextText: string;
43
+ }
44
+ export interface ApplyYjsTextUpdatesArgs {
45
+ previousStateBase64?: string | Uint8Array | null;
46
+ updates: readonly YjsServerUpdateEnvelope[];
47
+ containerKey?: string;
48
+ }
49
+ export interface ApplyYjsTextUpdatesResult {
50
+ nextStateBase64: string;
51
+ text: string;
52
+ }
53
+ export interface CreateYjsServerModuleOptions {
54
+ name?: string;
55
+ rules: readonly YjsServerFieldRule[];
56
+ envelopeKey?: string;
57
+ /**
58
+ * Throw when envelope payload references fields without matching rules.
59
+ * @default true
60
+ */
61
+ strict?: boolean;
62
+ /**
63
+ * Remove the Yjs envelope key from processed payload/rows.
64
+ * @default true
65
+ */
66
+ stripEnvelope?: boolean;
67
+ }
68
+ export interface CreateYjsServerPushPluginOptions extends CreateYjsServerModuleOptions {
69
+ priority?: number;
70
+ }
71
+ export interface YjsServerApplyPayloadArgs {
72
+ table: string;
73
+ rowId: string;
74
+ payload: Record<string, unknown>;
75
+ existingRow?: Record<string, unknown> | null;
76
+ }
77
+ export interface YjsServerMaterializeRowArgs {
78
+ table: string;
79
+ row: Record<string, unknown>;
80
+ }
81
+ export interface YjsServerModule {
82
+ name: string;
83
+ rules: readonly YjsServerFieldRule[];
84
+ envelopeKey: string;
85
+ applyPayload(args: YjsServerApplyPayloadArgs): Promise<Record<string, unknown>>;
86
+ materializeRow(args: YjsServerMaterializeRowArgs): Promise<Record<string, unknown>>;
87
+ }
88
+ export declare function buildYjsTextUpdate(args: BuildYjsTextUpdateArgs): Promise<BuildYjsTextUpdateResult>;
89
+ export declare function createYjsServerModule(options: CreateYjsServerModuleOptions): YjsServerModule;
90
+ export declare function createYjsServerPushPlugin(options: CreateYjsServerPushPluginOptions): SyncServerPushPlugin;
91
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,492 @@
1
+ import { ServerPushPluginPriority, } from '@syncular/server';
2
+ import { sql } from 'kysely';
3
+ import * as Y from 'yjs';
4
+ export const YJS_PAYLOAD_KEY = '__yjs';
5
+ const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
6
+ function isRecord(value) {
7
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ }
9
+ function readString(value) {
10
+ return typeof value === 'string' && value.length > 0 ? value : null;
11
+ }
12
+ function bytesToBase64(bytes) {
13
+ if (typeof Buffer !== 'undefined') {
14
+ return Buffer.from(bytes).toString('base64');
15
+ }
16
+ let binary = '';
17
+ for (const byte of bytes) {
18
+ binary += String.fromCharCode(byte);
19
+ }
20
+ return btoa(binary);
21
+ }
22
+ function tryReadBase64TextFromBytes(bytes) {
23
+ if (bytes.length === 0)
24
+ return null;
25
+ try {
26
+ const decoded = new TextDecoder().decode(bytes).trim();
27
+ if (!decoded || !BASE64_PATTERN.test(decoded))
28
+ return null;
29
+ return decoded;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ function base64ToBytes(base64) {
36
+ if (!BASE64_PATTERN.test(base64)) {
37
+ throw new Error('Invalid base64 string');
38
+ }
39
+ if (typeof Buffer !== 'undefined') {
40
+ return new Uint8Array(Buffer.from(base64, 'base64'));
41
+ }
42
+ const binary = atob(base64);
43
+ const out = new Uint8Array(binary.length);
44
+ for (let i = 0; i < binary.length; i++) {
45
+ out[i] = binary.charCodeAt(i);
46
+ }
47
+ return out;
48
+ }
49
+ function stateValueToBytes(value) {
50
+ if (value instanceof Uint8Array) {
51
+ const encoded = tryReadBase64TextFromBytes(value);
52
+ if (encoded)
53
+ return base64ToBytes(encoded);
54
+ return value;
55
+ }
56
+ const str = readString(value);
57
+ if (!str)
58
+ return null;
59
+ return base64ToBytes(str);
60
+ }
61
+ function stateValueToBase64(value) {
62
+ const str = readString(value);
63
+ if (str)
64
+ return str;
65
+ if (value instanceof Uint8Array) {
66
+ const encoded = tryReadBase64TextFromBytes(value);
67
+ if (encoded)
68
+ return encoded;
69
+ return bytesToBase64(value);
70
+ }
71
+ return null;
72
+ }
73
+ function createDocFromState(stateValue) {
74
+ const doc = new Y.Doc();
75
+ const bytes = stateValueToBytes(stateValue);
76
+ if (bytes && bytes.length > 0) {
77
+ Y.applyUpdate(doc, bytes);
78
+ }
79
+ return doc;
80
+ }
81
+ function exportSnapshotBase64(doc) {
82
+ return bytesToBase64(Y.encodeStateAsUpdate(doc));
83
+ }
84
+ function ensureTextContainer(doc, containerKey) {
85
+ return doc.getText(containerKey).toString();
86
+ }
87
+ function ensureXmlFragmentContainer(doc, containerKey) {
88
+ return doc.getXmlFragment(containerKey).toString();
89
+ }
90
+ function replaceText(doc, containerKey, nextText) {
91
+ const text = doc.getText(containerKey);
92
+ const currentLength = text.length;
93
+ doc.transact(() => {
94
+ if (currentLength > 0) {
95
+ text.delete(0, currentLength);
96
+ }
97
+ if (nextText.length > 0) {
98
+ text.insert(0, nextText);
99
+ }
100
+ });
101
+ }
102
+ function patchText(doc, containerKey, nextText) {
103
+ const text = doc.getText(containerKey);
104
+ const currentText = text.toString();
105
+ if (currentText === nextText)
106
+ return;
107
+ const minLength = Math.min(currentText.length, nextText.length);
108
+ let prefixLength = 0;
109
+ while (prefixLength < minLength &&
110
+ currentText.charCodeAt(prefixLength) === nextText.charCodeAt(prefixLength)) {
111
+ prefixLength += 1;
112
+ }
113
+ let currentSuffixStart = currentText.length;
114
+ let nextSuffixStart = nextText.length;
115
+ while (currentSuffixStart > prefixLength &&
116
+ nextSuffixStart > prefixLength &&
117
+ currentText.charCodeAt(currentSuffixStart - 1) ===
118
+ nextText.charCodeAt(nextSuffixStart - 1)) {
119
+ currentSuffixStart -= 1;
120
+ nextSuffixStart -= 1;
121
+ }
122
+ const deleteLength = currentSuffixStart - prefixLength;
123
+ const insertSegment = nextText.slice(prefixLength, nextSuffixStart);
124
+ doc.transact(() => {
125
+ if (deleteLength > 0) {
126
+ text.delete(prefixLength, deleteLength);
127
+ }
128
+ if (insertSegment.length > 0) {
129
+ text.insert(prefixLength, insertSegment);
130
+ }
131
+ });
132
+ }
133
+ function materializeRuleValue(doc, rule) {
134
+ if (rule.kind === 'text') {
135
+ return ensureTextContainer(doc, rule.containerKey);
136
+ }
137
+ return ensureXmlFragmentContainer(doc, rule.containerKey);
138
+ }
139
+ function seedRuleValueFromPayload(doc, rule, source) {
140
+ if (rule.kind !== 'text')
141
+ return;
142
+ const initialText = readString(source[rule.field]);
143
+ if (initialText) {
144
+ replaceText(doc, rule.containerKey, initialText);
145
+ }
146
+ }
147
+ function createUpdateId() {
148
+ const randomUUID = globalThis.crypto?.randomUUID;
149
+ if (typeof randomUUID === 'function') {
150
+ return randomUUID.call(globalThis.crypto);
151
+ }
152
+ const ts = Date.now().toString(36);
153
+ const rnd = Math.random().toString(36).slice(2, 12);
154
+ return `yjs-${ts}-${rnd}`;
155
+ }
156
+ function buildRuleIndex(rules) {
157
+ const index = new Map();
158
+ const normalizedRules = [];
159
+ const seen = new Set();
160
+ for (const rule of rules) {
161
+ if (!rule.table.trim()) {
162
+ throw new Error('YjsServerFieldRule.table cannot be empty');
163
+ }
164
+ if (!rule.field.trim()) {
165
+ throw new Error('YjsServerFieldRule.field cannot be empty');
166
+ }
167
+ if (!rule.stateColumn.trim()) {
168
+ throw new Error('YjsServerFieldRule.stateColumn cannot be empty');
169
+ }
170
+ const key = `${rule.table}\u001f${rule.field}`;
171
+ if (seen.has(key)) {
172
+ throw new Error(`Duplicate Yjs server rule for table "${rule.table}", field "${rule.field}"`);
173
+ }
174
+ seen.add(key);
175
+ const resolved = {
176
+ ...rule,
177
+ containerKey: rule.containerKey ?? rule.field,
178
+ rowIdField: rule.rowIdField ?? 'id',
179
+ kind: rule.kind ?? 'text',
180
+ };
181
+ normalizedRules.push(resolved);
182
+ const tableRules = index.get(resolved.table) ?? new Map();
183
+ tableRules.set(resolved.field, resolved);
184
+ index.set(resolved.table, tableRules);
185
+ }
186
+ return { index, normalizedRules };
187
+ }
188
+ function normalizeUpdateEnvelope(value, context) {
189
+ if (!isRecord(value)) {
190
+ throw new Error(`${context} must be an object`);
191
+ }
192
+ const updateId = readString(value.updateId);
193
+ const updateBase64 = readString(value.updateBase64);
194
+ if (!updateId) {
195
+ throw new Error(`${context}.updateId must be a non-empty string`);
196
+ }
197
+ if (!updateBase64) {
198
+ throw new Error(`${context}.updateBase64 must be a non-empty base64 string`);
199
+ }
200
+ return { updateId, updateBase64 };
201
+ }
202
+ function normalizeUpdateEnvelopes(value, context) {
203
+ if (Array.isArray(value)) {
204
+ return value.map((entry, i) => normalizeUpdateEnvelope(entry, `${context}[${i}]`));
205
+ }
206
+ return [normalizeUpdateEnvelope(value, context)];
207
+ }
208
+ /*async function applyYjsTextUpdates(
209
+ args: ApplyYjsTextUpdatesArgs
210
+ ): Promise<ApplyYjsTextUpdatesResult> {
211
+ const containerKey = args.containerKey ?? 'text';
212
+ const doc = createDocFromState(args.previousStateBase64);
213
+ try {
214
+ for (const update of args.updates) {
215
+ Y.applyUpdate(doc, base64ToBytes(update.updateBase64));
216
+ }
217
+ const text = ensureTextContainer(doc, containerKey);
218
+ const nextStateBase64 = exportSnapshotBase64(doc);
219
+ return { nextStateBase64, text };
220
+ } finally {
221
+ doc.destroy();
222
+ }
223
+ }*/
224
+ export async function buildYjsTextUpdate(args) {
225
+ const containerKey = args.containerKey ?? 'text';
226
+ const doc = createDocFromState(args.previousStateBase64);
227
+ try {
228
+ const from = Y.encodeStateVector(doc);
229
+ patchText(doc, containerKey, args.nextText);
230
+ const update = bytesToBase64(Y.encodeStateAsUpdate(doc, from));
231
+ const nextText = ensureTextContainer(doc, containerKey);
232
+ const nextStateBase64 = exportSnapshotBase64(doc);
233
+ return {
234
+ update: {
235
+ updateId: args.updateId ?? createUpdateId(),
236
+ updateBase64: update,
237
+ },
238
+ nextStateBase64,
239
+ nextText,
240
+ };
241
+ }
242
+ finally {
243
+ doc.destroy();
244
+ }
245
+ }
246
+ async function materializeRowFromState(args) {
247
+ const tableRules = args.index.get(args.table);
248
+ if (!tableRules) {
249
+ if (args.stripEnvelope && args.envelopeKey in args.row) {
250
+ const next = { ...args.row };
251
+ delete next[args.envelopeKey];
252
+ return next;
253
+ }
254
+ return args.row;
255
+ }
256
+ let nextRow = null;
257
+ const ensureRow = () => {
258
+ if (nextRow)
259
+ return nextRow;
260
+ nextRow = { ...args.row };
261
+ return nextRow;
262
+ };
263
+ for (const rule of tableRules.values()) {
264
+ const source = nextRow ?? args.row;
265
+ const stateBase64 = stateValueToBase64(source[rule.stateColumn]);
266
+ if (!stateBase64)
267
+ continue;
268
+ const doc = createDocFromState(stateBase64);
269
+ try {
270
+ const nextValue = materializeRuleValue(doc, rule);
271
+ if (source[rule.field] !== nextValue) {
272
+ ensureRow()[rule.field] = nextValue;
273
+ }
274
+ }
275
+ finally {
276
+ doc.destroy();
277
+ }
278
+ }
279
+ if (args.stripEnvelope) {
280
+ const source = nextRow ?? args.row;
281
+ if (args.envelopeKey in source) {
282
+ const target = ensureRow();
283
+ delete target[args.envelopeKey];
284
+ }
285
+ }
286
+ return nextRow ?? args.row;
287
+ }
288
+ async function applyYjsEnvelopeToPayload(args) {
289
+ const tableRules = args.index.get(args.table);
290
+ const rawEnvelope = args.payload[args.envelopeKey];
291
+ if (!tableRules) {
292
+ if (rawEnvelope !== undefined && args.strict) {
293
+ throw new Error(`Yjs envelope provided for table "${args.table}" without matching rules`);
294
+ }
295
+ if (args.stripEnvelope && rawEnvelope !== undefined) {
296
+ const next = { ...args.payload };
297
+ delete next[args.envelopeKey];
298
+ return next;
299
+ }
300
+ return args.payload;
301
+ }
302
+ let nextPayload = null;
303
+ const ensurePayload = () => {
304
+ if (nextPayload)
305
+ return nextPayload;
306
+ nextPayload = { ...args.payload };
307
+ return nextPayload;
308
+ };
309
+ const sourceEnvelope = rawEnvelope;
310
+ if (sourceEnvelope !== undefined && !isRecord(sourceEnvelope)) {
311
+ throw new Error(`Yjs payload key "${args.envelopeKey}" must be an object for table "${args.table}"`);
312
+ }
313
+ if (sourceEnvelope) {
314
+ for (const [field, rawUpdateInput] of Object.entries(sourceEnvelope)) {
315
+ const rule = tableRules.get(field);
316
+ if (!rule) {
317
+ if (args.strict) {
318
+ throw new Error(`No Yjs rule found for envelope field "${field}" on table "${args.table}"`);
319
+ }
320
+ continue;
321
+ }
322
+ const updates = normalizeUpdateEnvelopes(rawUpdateInput, `yjs.${args.table}.${field}`);
323
+ const source = nextPayload ?? args.payload;
324
+ const existingSource = args.existingRow ?? null;
325
+ const baseState = (existingSource
326
+ ? stateValueToBase64(existingSource[rule.stateColumn])
327
+ : null) ??
328
+ stateValueToBase64(source[rule.stateColumn]) ??
329
+ null;
330
+ const doc = createDocFromState(baseState);
331
+ try {
332
+ if (!baseState) {
333
+ seedRuleValueFromPayload(doc, rule, source);
334
+ }
335
+ for (const update of updates) {
336
+ Y.applyUpdate(doc, base64ToBytes(update.updateBase64));
337
+ }
338
+ const nextValue = materializeRuleValue(doc, rule);
339
+ const nextStateBase64 = exportSnapshotBase64(doc);
340
+ const target = ensurePayload();
341
+ target[rule.field] = nextValue;
342
+ target[rule.stateColumn] = nextStateBase64;
343
+ }
344
+ finally {
345
+ doc.destroy();
346
+ }
347
+ }
348
+ }
349
+ if (args.stripEnvelope) {
350
+ const source = nextPayload ?? args.payload;
351
+ if (args.envelopeKey in source) {
352
+ const target = ensurePayload();
353
+ delete target[args.envelopeKey];
354
+ }
355
+ }
356
+ return nextPayload ?? args.payload;
357
+ }
358
+ export function createYjsServerModule(options) {
359
+ if (options.rules.length === 0) {
360
+ throw new Error('createYjsServerModule requires at least one table/field rule');
361
+ }
362
+ const envelopeKey = options.envelopeKey ?? YJS_PAYLOAD_KEY;
363
+ const strict = options.strict ?? true;
364
+ const stripEnvelope = options.stripEnvelope ?? true;
365
+ const { index } = buildRuleIndex(options.rules);
366
+ return {
367
+ name: options.name ?? 'crdt-yjs-server',
368
+ rules: options.rules,
369
+ envelopeKey,
370
+ async applyPayload(args) {
371
+ return await applyYjsEnvelopeToPayload({
372
+ table: args.table,
373
+ payload: args.payload,
374
+ existingRow: args.existingRow,
375
+ index,
376
+ envelopeKey,
377
+ stripEnvelope,
378
+ strict,
379
+ });
380
+ },
381
+ async materializeRow(args) {
382
+ return await materializeRowFromState({
383
+ table: args.table,
384
+ row: args.row,
385
+ index,
386
+ envelopeKey,
387
+ stripEnvelope,
388
+ });
389
+ },
390
+ };
391
+ }
392
+ function buildTableRowIdFieldIndex(rules) {
393
+ const tableRowIdFields = new Map();
394
+ for (const rule of rules) {
395
+ const rowIdField = rule.rowIdField ?? 'id';
396
+ const existing = tableRowIdFields.get(rule.table);
397
+ if (existing && existing !== rowIdField) {
398
+ throw new Error(`Yjs rules for table "${rule.table}" must use a single rowIdField`);
399
+ }
400
+ tableRowIdFields.set(rule.table, rowIdField);
401
+ }
402
+ return tableRowIdFields;
403
+ }
404
+ async function materializeAppliedResult(yjsModule, opTable, applied) {
405
+ let nextResult = applied.result;
406
+ let resultChanged = false;
407
+ if (nextResult.status === 'conflict' && isRecord(nextResult.server_row)) {
408
+ const materializedServerRow = await yjsModule.materializeRow({
409
+ table: opTable,
410
+ row: nextResult.server_row,
411
+ });
412
+ if (materializedServerRow !== nextResult.server_row) {
413
+ nextResult = {
414
+ ...nextResult,
415
+ server_row: materializedServerRow,
416
+ };
417
+ resultChanged = true;
418
+ }
419
+ }
420
+ let emittedChanged = false;
421
+ const nextEmitted = [];
422
+ for (const emitted of applied.emittedChanges) {
423
+ if (emitted.op !== 'upsert' || !isRecord(emitted.row_json)) {
424
+ nextEmitted.push(emitted);
425
+ continue;
426
+ }
427
+ const materializedRow = await yjsModule.materializeRow({
428
+ table: emitted.table,
429
+ row: emitted.row_json,
430
+ });
431
+ if (materializedRow !== emitted.row_json) {
432
+ emittedChanged = true;
433
+ nextEmitted.push({
434
+ ...emitted,
435
+ row_json: materializedRow,
436
+ });
437
+ continue;
438
+ }
439
+ nextEmitted.push(emitted);
440
+ }
441
+ if (!resultChanged && !emittedChanged) {
442
+ return applied;
443
+ }
444
+ return {
445
+ result: nextResult,
446
+ emittedChanges: emittedChanged ? nextEmitted : applied.emittedChanges,
447
+ };
448
+ }
449
+ export function createYjsServerPushPlugin(options) {
450
+ const yjsModule = createYjsServerModule(options);
451
+ const tableRowIdFields = buildTableRowIdFieldIndex(options.rules);
452
+ return {
453
+ name: options.name ?? yjsModule.name,
454
+ priority: options.priority ?? ServerPushPluginPriority.CRDT,
455
+ async beforeApplyOperation(args) {
456
+ const op = args.op;
457
+ if (op.op !== 'upsert' || !isRecord(op.payload)) {
458
+ return op;
459
+ }
460
+ const rowIdField = tableRowIdFields.get(op.table);
461
+ let existingRow = null;
462
+ if (rowIdField) {
463
+ const loadedRows = await sql `
464
+ select *
465
+ from ${sql.table(op.table)}
466
+ where ${sql.ref(rowIdField)} = ${sql.val(op.row_id)}
467
+ limit ${sql.val(1)}
468
+ `.execute(args.ctx.trx);
469
+ const loadedRow = loadedRows.rows[0];
470
+ if (loadedRow && isRecord(loadedRow)) {
471
+ existingRow = loadedRow;
472
+ }
473
+ }
474
+ const nextPayload = await yjsModule.applyPayload({
475
+ table: op.table,
476
+ rowId: op.row_id,
477
+ payload: op.payload,
478
+ existingRow,
479
+ });
480
+ if (nextPayload === op.payload)
481
+ return op;
482
+ return {
483
+ ...op,
484
+ payload: nextPayload,
485
+ };
486
+ },
487
+ async afterApplyOperation(args) {
488
+ return await materializeAppliedResult(yjsModule, args.op.table, args.applied);
489
+ },
490
+ };
491
+ }
492
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/server-plugin-yjs",
3
- "version": "0.0.0",
3
+ "version": "0.0.6-165",
4
4
  "description": "Yjs CRDT server integration primitives for Syncular handlers",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -51,7 +51,7 @@
51
51
  "src"
52
52
  ],
53
53
  "dependencies": {
54
- "@syncular/server": "0.0.0",
54
+ "@syncular/server": "0.0.6-165",
55
55
  "kysely": "^0.28.11",
56
56
  "yjs": "^13.6.29"
57
57
  }
package/src/index.test.ts CHANGED
@@ -61,7 +61,50 @@ function createXmlInsert(
61
61
  };
62
62
  }
63
63
 
64
+ function applyTextUpdates(args: {
65
+ previousStateBase64?: string;
66
+ updates: readonly YjsServerUpdateEnvelope[];
67
+ containerKey?: string;
68
+ }): string {
69
+ const doc = new Y.Doc();
70
+ if (args.previousStateBase64) {
71
+ Y.applyUpdate(
72
+ doc,
73
+ new Uint8Array(Buffer.from(args.previousStateBase64, 'base64'))
74
+ );
75
+ }
76
+ for (const update of args.updates) {
77
+ Y.applyUpdate(
78
+ doc,
79
+ new Uint8Array(Buffer.from(update.updateBase64, 'base64'))
80
+ );
81
+ }
82
+ const text = doc.getText(args.containerKey ?? 'content').toString();
83
+ doc.destroy();
84
+ return text;
85
+ }
86
+
64
87
  describe('@syncular/server-plugin-crdt-yjs', () => {
88
+ it('buildYjsTextUpdate merges concurrent prepend and append without duplication', async () => {
89
+ const base = await createUpdate('123');
90
+ const prepend = await createUpdate('0123', base.state);
91
+ const append = await createUpdate('1234', base.state);
92
+
93
+ const mergedForward = applyTextUpdates({
94
+ previousStateBase64: base.state,
95
+ updates: [prepend.update, append.update],
96
+ containerKey: 'content',
97
+ });
98
+ const mergedReverse = applyTextUpdates({
99
+ previousStateBase64: base.state,
100
+ updates: [append.update, prepend.update],
101
+ containerKey: 'content',
102
+ });
103
+
104
+ expect(mergedForward).toBe('01234');
105
+ expect(mergedReverse).toBe('01234');
106
+ });
107
+
65
108
  it('applies Yjs envelopes against existing row state and strips envelope key', async () => {
66
109
  const module = createYjsServerModule({
67
110
  rules: [
package/src/index.ts CHANGED
@@ -229,6 +229,45 @@ function replaceText(doc: Y.Doc, containerKey: string, nextText: string): void {
229
229
  });
230
230
  }
231
231
 
232
+ function patchText(doc: Y.Doc, containerKey: string, nextText: string): void {
233
+ const text = doc.getText(containerKey);
234
+ const currentText = text.toString();
235
+ if (currentText === nextText) return;
236
+
237
+ const minLength = Math.min(currentText.length, nextText.length);
238
+ let prefixLength = 0;
239
+ while (
240
+ prefixLength < minLength &&
241
+ currentText.charCodeAt(prefixLength) === nextText.charCodeAt(prefixLength)
242
+ ) {
243
+ prefixLength += 1;
244
+ }
245
+
246
+ let currentSuffixStart = currentText.length;
247
+ let nextSuffixStart = nextText.length;
248
+ while (
249
+ currentSuffixStart > prefixLength &&
250
+ nextSuffixStart > prefixLength &&
251
+ currentText.charCodeAt(currentSuffixStart - 1) ===
252
+ nextText.charCodeAt(nextSuffixStart - 1)
253
+ ) {
254
+ currentSuffixStart -= 1;
255
+ nextSuffixStart -= 1;
256
+ }
257
+
258
+ const deleteLength = currentSuffixStart - prefixLength;
259
+ const insertSegment = nextText.slice(prefixLength, nextSuffixStart);
260
+
261
+ doc.transact(() => {
262
+ if (deleteLength > 0) {
263
+ text.delete(prefixLength, deleteLength);
264
+ }
265
+ if (insertSegment.length > 0) {
266
+ text.insert(prefixLength, insertSegment);
267
+ }
268
+ });
269
+ }
270
+
232
271
  function materializeRuleValue(
233
272
  doc: Y.Doc,
234
273
  rule: ResolvedYjsServerFieldRule
@@ -360,7 +399,7 @@ export async function buildYjsTextUpdate(
360
399
  const doc = createDocFromState(args.previousStateBase64);
361
400
  try {
362
401
  const from = Y.encodeStateVector(doc);
363
- replaceText(doc, containerKey, args.nextText);
402
+ patchText(doc, containerKey, args.nextText);
364
403
  const update = bytesToBase64(Y.encodeStateAsUpdate(doc, from));
365
404
  const nextText = ensureTextContainer(doc, containerKey);
366
405
  const nextStateBase64 = exportSnapshotBase64(doc);