@syncular/client-plugin-yjs 0.0.0 → 0.0.6-159
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 +2 -2
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +592 -0
- package/dist/index.js.map +1 -0
- package/package.json +2 -2
- package/src/index.test.ts +183 -0
- package/src/index.ts +93 -5
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @syncular/client-plugin-
|
|
1
|
+
# @syncular/client-plugin-yjs
|
|
2
2
|
|
|
3
3
|
Yjs-first CRDT plugin for Syncular client integration.
|
|
4
4
|
|
|
@@ -45,7 +45,7 @@ The plugin converts this to:
|
|
|
45
45
|
## Example
|
|
46
46
|
|
|
47
47
|
```ts
|
|
48
|
-
import { createYjsClientPlugin } from '@syncular/client-plugin-
|
|
48
|
+
import { createYjsClientPlugin } from '@syncular/client-plugin-yjs';
|
|
49
49
|
|
|
50
50
|
const yjs = createYjsClientPlugin({
|
|
51
51
|
rules: [
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type SyncClientPlugin } from '@syncular/client';
|
|
2
|
+
export declare const YJS_PAYLOAD_KEY = "__yjs";
|
|
3
|
+
export type YjsFieldKind = 'text' | 'xml-fragment' | 'prosemirror';
|
|
4
|
+
export interface YjsClientFieldRule {
|
|
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 YjsClientUpdateEnvelope {
|
|
26
|
+
updateId: string;
|
|
27
|
+
updateBase64: string;
|
|
28
|
+
}
|
|
29
|
+
export type YjsClientUpdateInput = YjsClientUpdateEnvelope | readonly YjsClientUpdateEnvelope[];
|
|
30
|
+
export interface YjsClientPayloadEnvelope {
|
|
31
|
+
[field: string]: YjsClientUpdateInput;
|
|
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: YjsClientUpdateEnvelope;
|
|
41
|
+
nextStateBase64: string;
|
|
42
|
+
nextText: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ApplyYjsTextUpdatesArgs {
|
|
45
|
+
previousStateBase64?: string | Uint8Array | null;
|
|
46
|
+
updates: readonly YjsClientUpdateEnvelope[];
|
|
47
|
+
containerKey?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ApplyYjsTextUpdatesResult {
|
|
50
|
+
nextStateBase64: string;
|
|
51
|
+
text: string;
|
|
52
|
+
}
|
|
53
|
+
export interface CreateYjsClientPluginOptions {
|
|
54
|
+
name?: string;
|
|
55
|
+
rules: readonly YjsClientFieldRule[];
|
|
56
|
+
envelopeKey?: string;
|
|
57
|
+
priority?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Maximum cached row-field Yjs states to keep in memory.
|
|
60
|
+
* Prevents unbounded growth in long-lived sessions.
|
|
61
|
+
* @default 10_000
|
|
62
|
+
*/
|
|
63
|
+
maxTrackedRows?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Throw when envelope payload references fields without matching rules.
|
|
66
|
+
* @default true
|
|
67
|
+
*/
|
|
68
|
+
strict?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Remove the Yjs envelope key from outgoing/incoming records.
|
|
71
|
+
* @default true
|
|
72
|
+
*/
|
|
73
|
+
stripEnvelope?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Remove the Yjs envelope key from push payloads.
|
|
76
|
+
* Default inherits from `stripEnvelope`.
|
|
77
|
+
*/
|
|
78
|
+
stripEnvelopeBeforePush?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Remove the Yjs envelope key from local optimistic mutation payloads.
|
|
81
|
+
* Default inherits from `stripEnvelope`.
|
|
82
|
+
*/
|
|
83
|
+
stripEnvelopeBeforeApplyLocalMutations?: boolean;
|
|
84
|
+
}
|
|
85
|
+
export declare function applyYjsTextUpdates(args: ApplyYjsTextUpdatesArgs): ApplyYjsTextUpdatesResult;
|
|
86
|
+
export declare function buildYjsTextUpdate(args: BuildYjsTextUpdateArgs): BuildYjsTextUpdateResult;
|
|
87
|
+
export declare function createYjsClientPlugin(options: CreateYjsClientPluginOptions): SyncClientPlugin;
|
|
88
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,gBAAgB,EAKtB,MAAM,kBAAkB,CAAC;AAG1B,eAAO,MAAM,eAAe,UAAU,CAAC;AAKvC,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,cAAc,GAAG,aAAa,CAAC;AAEnE,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;OAEG;IACH,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AAQD,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,oBAAoB,GAC5B,uBAAuB,GACvB,SAAS,uBAAuB,EAAE,CAAC;AAEvC,MAAM,WAAW,wBAAwB;IACvC,CAAC,KAAK,EAAE,MAAM,GAAG,oBAAoB,CAAC;CACvC;AAED,MAAM,WAAW,sBAAsB;IACrC,mBAAmB,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,uBAAuB,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,mBAAmB,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;IACjD,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAC5C,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,yBAAyB;IACxC,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,4BAA4B;IAC3C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC;;;OAGG;IACH,sCAAsC,CAAC,EAAE,OAAO,CAAC;CAClD;AA0SD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,uBAAuB,GAC5B,yBAAyB,CAa3B;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,sBAAsB,GAC3B,wBAAwB,CAqB1B;AAsSD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,gBAAgB,CA0HlB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import { PluginPriority, } from '@syncular/client';
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
export const YJS_PAYLOAD_KEY = '__yjs';
|
|
4
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function readString(value) {
|
|
9
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
10
|
+
}
|
|
11
|
+
function bytesToBase64(bytes) {
|
|
12
|
+
if (typeof Buffer !== 'undefined') {
|
|
13
|
+
return Buffer.from(bytes).toString('base64');
|
|
14
|
+
}
|
|
15
|
+
let binary = '';
|
|
16
|
+
for (const byte of bytes) {
|
|
17
|
+
binary += String.fromCharCode(byte);
|
|
18
|
+
}
|
|
19
|
+
return btoa(binary);
|
|
20
|
+
}
|
|
21
|
+
function tryReadBase64TextFromBytes(bytes) {
|
|
22
|
+
if (bytes.length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
const decoded = new TextDecoder().decode(bytes).trim();
|
|
26
|
+
if (!decoded || !BASE64_PATTERN.test(decoded))
|
|
27
|
+
return null;
|
|
28
|
+
return decoded;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function base64ToBytes(base64) {
|
|
35
|
+
if (!BASE64_PATTERN.test(base64)) {
|
|
36
|
+
throw new Error('Invalid base64 string');
|
|
37
|
+
}
|
|
38
|
+
if (typeof Buffer !== 'undefined') {
|
|
39
|
+
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
40
|
+
}
|
|
41
|
+
const binary = atob(base64);
|
|
42
|
+
const out = new Uint8Array(binary.length);
|
|
43
|
+
for (let i = 0; i < binary.length; i++) {
|
|
44
|
+
out[i] = binary.charCodeAt(i);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
function stateValueToBytes(value) {
|
|
49
|
+
if (value instanceof Uint8Array) {
|
|
50
|
+
const encoded = tryReadBase64TextFromBytes(value);
|
|
51
|
+
if (encoded)
|
|
52
|
+
return base64ToBytes(encoded);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
const str = readString(value);
|
|
56
|
+
if (!str)
|
|
57
|
+
return null;
|
|
58
|
+
return base64ToBytes(str);
|
|
59
|
+
}
|
|
60
|
+
function stateValueToBase64(value) {
|
|
61
|
+
const str = readString(value);
|
|
62
|
+
if (str)
|
|
63
|
+
return str;
|
|
64
|
+
if (value instanceof Uint8Array) {
|
|
65
|
+
const encoded = tryReadBase64TextFromBytes(value);
|
|
66
|
+
if (encoded)
|
|
67
|
+
return encoded;
|
|
68
|
+
return bytesToBase64(value);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function createDocFromState(stateValue) {
|
|
73
|
+
const doc = new Y.Doc();
|
|
74
|
+
const bytes = stateValueToBytes(stateValue);
|
|
75
|
+
if (bytes && bytes.length > 0) {
|
|
76
|
+
Y.applyUpdate(doc, bytes);
|
|
77
|
+
}
|
|
78
|
+
return doc;
|
|
79
|
+
}
|
|
80
|
+
function exportSnapshotBase64(doc) {
|
|
81
|
+
return bytesToBase64(Y.encodeStateAsUpdate(doc));
|
|
82
|
+
}
|
|
83
|
+
function replaceText(doc, containerKey, nextText) {
|
|
84
|
+
const text = doc.getText(containerKey);
|
|
85
|
+
const currentLength = text.length;
|
|
86
|
+
doc.transact(() => {
|
|
87
|
+
if (currentLength > 0) {
|
|
88
|
+
text.delete(0, currentLength);
|
|
89
|
+
}
|
|
90
|
+
if (nextText.length > 0) {
|
|
91
|
+
text.insert(0, nextText);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function patchText(doc, containerKey, nextText) {
|
|
96
|
+
const text = doc.getText(containerKey);
|
|
97
|
+
const currentText = text.toString();
|
|
98
|
+
if (currentText === nextText)
|
|
99
|
+
return;
|
|
100
|
+
const minLength = Math.min(currentText.length, nextText.length);
|
|
101
|
+
let prefixLength = 0;
|
|
102
|
+
while (prefixLength < minLength &&
|
|
103
|
+
currentText.charCodeAt(prefixLength) === nextText.charCodeAt(prefixLength)) {
|
|
104
|
+
prefixLength += 1;
|
|
105
|
+
}
|
|
106
|
+
let currentSuffixStart = currentText.length;
|
|
107
|
+
let nextSuffixStart = nextText.length;
|
|
108
|
+
while (currentSuffixStart > prefixLength &&
|
|
109
|
+
nextSuffixStart > prefixLength &&
|
|
110
|
+
currentText.charCodeAt(currentSuffixStart - 1) ===
|
|
111
|
+
nextText.charCodeAt(nextSuffixStart - 1)) {
|
|
112
|
+
currentSuffixStart -= 1;
|
|
113
|
+
nextSuffixStart -= 1;
|
|
114
|
+
}
|
|
115
|
+
const deleteLength = currentSuffixStart - prefixLength;
|
|
116
|
+
const insertSegment = nextText.slice(prefixLength, nextSuffixStart);
|
|
117
|
+
doc.transact(() => {
|
|
118
|
+
if (deleteLength > 0) {
|
|
119
|
+
text.delete(prefixLength, deleteLength);
|
|
120
|
+
}
|
|
121
|
+
if (insertSegment.length > 0) {
|
|
122
|
+
text.insert(prefixLength, insertSegment);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function ensureTextContainer(doc, containerKey) {
|
|
127
|
+
return doc.getText(containerKey).toString();
|
|
128
|
+
}
|
|
129
|
+
function ensureXmlFragmentContainer(doc, containerKey) {
|
|
130
|
+
return doc.getXmlFragment(containerKey).toString();
|
|
131
|
+
}
|
|
132
|
+
function materializeRuleValue(doc, rule) {
|
|
133
|
+
if (rule.kind === 'text') {
|
|
134
|
+
return ensureTextContainer(doc, rule.containerKey);
|
|
135
|
+
}
|
|
136
|
+
return ensureXmlFragmentContainer(doc, rule.containerKey);
|
|
137
|
+
}
|
|
138
|
+
function seedRuleValueFromPayload(doc, rule, source) {
|
|
139
|
+
if (rule.kind !== 'text')
|
|
140
|
+
return;
|
|
141
|
+
const initialText = readString(source[rule.field]);
|
|
142
|
+
if (initialText) {
|
|
143
|
+
replaceText(doc, rule.containerKey, initialText);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function buildRuleIndex(rules) {
|
|
147
|
+
const index = new Map();
|
|
148
|
+
const normalizedRules = [];
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
for (const rule of rules) {
|
|
151
|
+
if (!rule.table.trim()) {
|
|
152
|
+
throw new Error('YjsClientFieldRule.table cannot be empty');
|
|
153
|
+
}
|
|
154
|
+
if (!rule.field.trim()) {
|
|
155
|
+
throw new Error('YjsClientFieldRule.field cannot be empty');
|
|
156
|
+
}
|
|
157
|
+
if (!rule.stateColumn.trim()) {
|
|
158
|
+
throw new Error('YjsClientFieldRule.stateColumn cannot be empty');
|
|
159
|
+
}
|
|
160
|
+
const key = `${rule.table}\u001f${rule.field}`;
|
|
161
|
+
if (seen.has(key)) {
|
|
162
|
+
throw new Error(`Duplicate Yjs client rule for table "${rule.table}", field "${rule.field}"`);
|
|
163
|
+
}
|
|
164
|
+
seen.add(key);
|
|
165
|
+
const resolved = {
|
|
166
|
+
...rule,
|
|
167
|
+
containerKey: rule.containerKey ?? rule.field,
|
|
168
|
+
rowIdField: rule.rowIdField ?? 'id',
|
|
169
|
+
kind: rule.kind ?? 'text',
|
|
170
|
+
};
|
|
171
|
+
normalizedRules.push(resolved);
|
|
172
|
+
const tableRules = index.get(resolved.table) ?? new Map();
|
|
173
|
+
tableRules.set(resolved.field, resolved);
|
|
174
|
+
index.set(resolved.table, tableRules);
|
|
175
|
+
}
|
|
176
|
+
return { index, normalizedRules };
|
|
177
|
+
}
|
|
178
|
+
function rowFieldCacheKey(table, rowId, field) {
|
|
179
|
+
return `${table}\u001f${rowId}\u001f${field}`;
|
|
180
|
+
}
|
|
181
|
+
function touchLruState(map, key, value, maxTrackedRows) {
|
|
182
|
+
map.delete(key);
|
|
183
|
+
map.set(key, value);
|
|
184
|
+
if (map.size <= maxTrackedRows)
|
|
185
|
+
return;
|
|
186
|
+
const oldest = map.keys().next().value;
|
|
187
|
+
if (oldest) {
|
|
188
|
+
map.delete(oldest);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function clearRowStateCache(args) {
|
|
192
|
+
const tableRules = args.index.get(args.table);
|
|
193
|
+
if (!tableRules)
|
|
194
|
+
return;
|
|
195
|
+
for (const rule of tableRules.values()) {
|
|
196
|
+
args.stateByRowField.delete(rowFieldCacheKey(args.table, args.rowId, rule.field));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function resolveSnapshotRowId(row, rule) {
|
|
200
|
+
const candidate = row[rule.rowIdField];
|
|
201
|
+
return typeof candidate === 'string' && candidate.length > 0
|
|
202
|
+
? candidate
|
|
203
|
+
: null;
|
|
204
|
+
}
|
|
205
|
+
function normalizeUpdateEnvelope(value, context) {
|
|
206
|
+
if (!isRecord(value)) {
|
|
207
|
+
throw new Error(`${context} must be an object`);
|
|
208
|
+
}
|
|
209
|
+
const updateId = readString(value.updateId);
|
|
210
|
+
const updateBase64 = readString(value.updateBase64);
|
|
211
|
+
if (!updateId) {
|
|
212
|
+
throw new Error(`${context}.updateId must be a non-empty string`);
|
|
213
|
+
}
|
|
214
|
+
if (!updateBase64) {
|
|
215
|
+
throw new Error(`${context}.updateBase64 must be a non-empty base64 string`);
|
|
216
|
+
}
|
|
217
|
+
return { updateId, updateBase64 };
|
|
218
|
+
}
|
|
219
|
+
function normalizeUpdateEnvelopes(value, context) {
|
|
220
|
+
if (Array.isArray(value)) {
|
|
221
|
+
return value.map((entry, i) => normalizeUpdateEnvelope(entry, `${context}[${i}]`));
|
|
222
|
+
}
|
|
223
|
+
return [normalizeUpdateEnvelope(value, context)];
|
|
224
|
+
}
|
|
225
|
+
function createUpdateId() {
|
|
226
|
+
const randomUUID = globalThis.crypto?.randomUUID;
|
|
227
|
+
if (typeof randomUUID === 'function') {
|
|
228
|
+
return randomUUID.call(globalThis.crypto);
|
|
229
|
+
}
|
|
230
|
+
const ts = Date.now().toString(36);
|
|
231
|
+
const rnd = Math.random().toString(36).slice(2, 12);
|
|
232
|
+
return `yjs-${ts}-${rnd}`;
|
|
233
|
+
}
|
|
234
|
+
export function applyYjsTextUpdates(args) {
|
|
235
|
+
const containerKey = args.containerKey ?? 'text';
|
|
236
|
+
const doc = createDocFromState(args.previousStateBase64);
|
|
237
|
+
try {
|
|
238
|
+
for (const update of args.updates) {
|
|
239
|
+
Y.applyUpdate(doc, base64ToBytes(update.updateBase64));
|
|
240
|
+
}
|
|
241
|
+
const text = ensureTextContainer(doc, containerKey);
|
|
242
|
+
const nextStateBase64 = exportSnapshotBase64(doc);
|
|
243
|
+
return { nextStateBase64, text };
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
doc.destroy();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
export function buildYjsTextUpdate(args) {
|
|
250
|
+
const containerKey = args.containerKey ?? 'text';
|
|
251
|
+
const doc = createDocFromState(args.previousStateBase64);
|
|
252
|
+
try {
|
|
253
|
+
const from = Y.encodeStateVector(doc);
|
|
254
|
+
patchText(doc, containerKey, args.nextText);
|
|
255
|
+
const update = bytesToBase64(Y.encodeStateAsUpdate(doc, from));
|
|
256
|
+
const nextText = ensureTextContainer(doc, containerKey);
|
|
257
|
+
const nextStateBase64 = exportSnapshotBase64(doc);
|
|
258
|
+
return {
|
|
259
|
+
update: {
|
|
260
|
+
updateId: args.updateId ?? createUpdateId(),
|
|
261
|
+
updateBase64: update,
|
|
262
|
+
},
|
|
263
|
+
nextStateBase64,
|
|
264
|
+
nextText,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
doc.destroy();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function materializeRowFromState(args) {
|
|
272
|
+
const tableRules = args.index.get(args.table);
|
|
273
|
+
if (!tableRules) {
|
|
274
|
+
if (args.stripEnvelope && args.envelopeKey in args.row) {
|
|
275
|
+
const next = { ...args.row };
|
|
276
|
+
delete next[args.envelopeKey];
|
|
277
|
+
return next;
|
|
278
|
+
}
|
|
279
|
+
return args.row;
|
|
280
|
+
}
|
|
281
|
+
let nextRow = null;
|
|
282
|
+
const ensureRow = () => {
|
|
283
|
+
if (nextRow)
|
|
284
|
+
return nextRow;
|
|
285
|
+
nextRow = { ...args.row };
|
|
286
|
+
return nextRow;
|
|
287
|
+
};
|
|
288
|
+
for (const rule of tableRules.values()) {
|
|
289
|
+
const source = nextRow ?? args.row;
|
|
290
|
+
const stateBase64 = stateValueToBase64(source[rule.stateColumn]);
|
|
291
|
+
if (!stateBase64)
|
|
292
|
+
continue;
|
|
293
|
+
const doc = createDocFromState(stateBase64);
|
|
294
|
+
try {
|
|
295
|
+
const nextValue = materializeRuleValue(doc, rule);
|
|
296
|
+
if (source[rule.field] !== nextValue) {
|
|
297
|
+
ensureRow()[rule.field] = nextValue;
|
|
298
|
+
}
|
|
299
|
+
if (args.rowId) {
|
|
300
|
+
touchLruState(args.stateByRowField, rowFieldCacheKey(args.table, args.rowId, rule.field), stateBase64, args.maxTrackedRows);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
finally {
|
|
304
|
+
doc.destroy();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (args.stripEnvelope) {
|
|
308
|
+
const source = nextRow ?? args.row;
|
|
309
|
+
if (args.envelopeKey in source) {
|
|
310
|
+
const target = ensureRow();
|
|
311
|
+
delete target[args.envelopeKey];
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return nextRow ?? args.row;
|
|
315
|
+
}
|
|
316
|
+
function transformPushPayload(args) {
|
|
317
|
+
const tableRules = args.index.get(args.table);
|
|
318
|
+
const rawEnvelope = args.payload[args.envelopeKey];
|
|
319
|
+
if (!tableRules) {
|
|
320
|
+
if (rawEnvelope !== undefined && args.strict) {
|
|
321
|
+
throw new Error(`Yjs envelope provided for table "${args.table}" without matching rules`);
|
|
322
|
+
}
|
|
323
|
+
if (args.stripEnvelope && rawEnvelope !== undefined) {
|
|
324
|
+
const next = { ...args.payload };
|
|
325
|
+
delete next[args.envelopeKey];
|
|
326
|
+
return next;
|
|
327
|
+
}
|
|
328
|
+
return args.payload;
|
|
329
|
+
}
|
|
330
|
+
let nextPayload = null;
|
|
331
|
+
const ensurePayload = () => {
|
|
332
|
+
if (nextPayload)
|
|
333
|
+
return nextPayload;
|
|
334
|
+
nextPayload = { ...args.payload };
|
|
335
|
+
return nextPayload;
|
|
336
|
+
};
|
|
337
|
+
const sourceEnvelope = rawEnvelope;
|
|
338
|
+
if (sourceEnvelope !== undefined && !isRecord(sourceEnvelope)) {
|
|
339
|
+
throw new Error(`Yjs payload key "${args.envelopeKey}" must be an object for table "${args.table}"`);
|
|
340
|
+
}
|
|
341
|
+
for (const rule of tableRules.values()) {
|
|
342
|
+
const source = nextPayload ?? args.payload;
|
|
343
|
+
const stateBase64 = stateValueToBase64(source[rule.stateColumn]);
|
|
344
|
+
if (stateBase64) {
|
|
345
|
+
touchLruState(args.stateByRowField, rowFieldCacheKey(args.table, args.rowId, rule.field), stateBase64, args.maxTrackedRows);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (sourceEnvelope) {
|
|
349
|
+
for (const [field, rawUpdateInput] of Object.entries(sourceEnvelope)) {
|
|
350
|
+
const rule = tableRules.get(field);
|
|
351
|
+
if (!rule) {
|
|
352
|
+
if (args.strict) {
|
|
353
|
+
throw new Error(`No Yjs rule found for envelope field "${field}" on table "${args.table}"`);
|
|
354
|
+
}
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const updates = normalizeUpdateEnvelopes(rawUpdateInput, `yjs.${args.table}.${field}`);
|
|
358
|
+
const cacheKey = rowFieldCacheKey(args.table, args.rowId, rule.field);
|
|
359
|
+
const source = nextPayload ?? args.payload;
|
|
360
|
+
const baseState = stateValueToBase64(source[rule.stateColumn]) ??
|
|
361
|
+
args.stateByRowField.get(cacheKey) ??
|
|
362
|
+
null;
|
|
363
|
+
const doc = createDocFromState(baseState);
|
|
364
|
+
try {
|
|
365
|
+
if (!baseState) {
|
|
366
|
+
seedRuleValueFromPayload(doc, rule, source);
|
|
367
|
+
}
|
|
368
|
+
for (const update of updates) {
|
|
369
|
+
Y.applyUpdate(doc, base64ToBytes(update.updateBase64));
|
|
370
|
+
}
|
|
371
|
+
const nextValue = materializeRuleValue(doc, rule);
|
|
372
|
+
const nextStateBase64 = exportSnapshotBase64(doc);
|
|
373
|
+
const target = ensurePayload();
|
|
374
|
+
target[rule.field] = nextValue;
|
|
375
|
+
target[rule.stateColumn] = nextStateBase64;
|
|
376
|
+
touchLruState(args.stateByRowField, cacheKey, nextStateBase64, args.maxTrackedRows);
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
doc.destroy();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (args.stripEnvelope) {
|
|
384
|
+
const source = nextPayload ?? args.payload;
|
|
385
|
+
if (args.envelopeKey in source) {
|
|
386
|
+
const target = ensurePayload();
|
|
387
|
+
delete target[args.envelopeKey];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return nextPayload ?? args.payload;
|
|
391
|
+
}
|
|
392
|
+
function transformPullSubscription(args) {
|
|
393
|
+
const nextSnapshots = (args.sub.snapshots ?? []).map((snapshot) => ({
|
|
394
|
+
...snapshot,
|
|
395
|
+
rows: (snapshot.rows ?? []).map((row) => {
|
|
396
|
+
if (!isRecord(row))
|
|
397
|
+
return row;
|
|
398
|
+
const tableRules = args.index.get(snapshot.table);
|
|
399
|
+
if (!tableRules) {
|
|
400
|
+
return materializeRowFromState({
|
|
401
|
+
table: snapshot.table,
|
|
402
|
+
rowId: null,
|
|
403
|
+
row,
|
|
404
|
+
index: args.index,
|
|
405
|
+
stateByRowField: args.stateByRowField,
|
|
406
|
+
maxTrackedRows: args.maxTrackedRows,
|
|
407
|
+
envelopeKey: args.envelopeKey,
|
|
408
|
+
stripEnvelope: args.stripEnvelope,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
let rowId = null;
|
|
412
|
+
for (const rule of tableRules.values()) {
|
|
413
|
+
rowId = resolveSnapshotRowId(row, rule);
|
|
414
|
+
if (rowId)
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
return materializeRowFromState({
|
|
418
|
+
table: snapshot.table,
|
|
419
|
+
rowId,
|
|
420
|
+
row,
|
|
421
|
+
index: args.index,
|
|
422
|
+
stateByRowField: args.stateByRowField,
|
|
423
|
+
maxTrackedRows: args.maxTrackedRows,
|
|
424
|
+
envelopeKey: args.envelopeKey,
|
|
425
|
+
stripEnvelope: args.stripEnvelope,
|
|
426
|
+
});
|
|
427
|
+
}),
|
|
428
|
+
}));
|
|
429
|
+
const nextCommits = (args.sub.commits ?? []).map((commit) => ({
|
|
430
|
+
...commit,
|
|
431
|
+
changes: (commit.changes ?? []).map((change) => {
|
|
432
|
+
if (change.op === 'delete') {
|
|
433
|
+
clearRowStateCache({
|
|
434
|
+
table: change.table,
|
|
435
|
+
rowId: change.row_id,
|
|
436
|
+
index: args.index,
|
|
437
|
+
stateByRowField: args.stateByRowField,
|
|
438
|
+
});
|
|
439
|
+
return change;
|
|
440
|
+
}
|
|
441
|
+
if (change.op !== 'upsert' || !isRecord(change.row_json))
|
|
442
|
+
return change;
|
|
443
|
+
const nextRow = materializeRowFromState({
|
|
444
|
+
table: change.table,
|
|
445
|
+
rowId: change.row_id,
|
|
446
|
+
row: change.row_json,
|
|
447
|
+
index: args.index,
|
|
448
|
+
stateByRowField: args.stateByRowField,
|
|
449
|
+
maxTrackedRows: args.maxTrackedRows,
|
|
450
|
+
envelopeKey: args.envelopeKey,
|
|
451
|
+
stripEnvelope: args.stripEnvelope,
|
|
452
|
+
});
|
|
453
|
+
if (nextRow === change.row_json)
|
|
454
|
+
return change;
|
|
455
|
+
return { ...change, row_json: nextRow };
|
|
456
|
+
}),
|
|
457
|
+
}));
|
|
458
|
+
return {
|
|
459
|
+
...args.sub,
|
|
460
|
+
snapshots: nextSnapshots,
|
|
461
|
+
commits: nextCommits,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
function transformWsChanges(args) {
|
|
465
|
+
return args.changes.map((change) => {
|
|
466
|
+
if (change.op === 'delete') {
|
|
467
|
+
clearRowStateCache({
|
|
468
|
+
table: change.table,
|
|
469
|
+
rowId: change.row_id,
|
|
470
|
+
index: args.index,
|
|
471
|
+
stateByRowField: args.stateByRowField,
|
|
472
|
+
});
|
|
473
|
+
return change;
|
|
474
|
+
}
|
|
475
|
+
if (change.op !== 'upsert' || !isRecord(change.row_json))
|
|
476
|
+
return change;
|
|
477
|
+
const nextRow = materializeRowFromState({
|
|
478
|
+
table: change.table,
|
|
479
|
+
rowId: change.row_id,
|
|
480
|
+
row: change.row_json,
|
|
481
|
+
index: args.index,
|
|
482
|
+
stateByRowField: args.stateByRowField,
|
|
483
|
+
maxTrackedRows: args.maxTrackedRows,
|
|
484
|
+
envelopeKey: args.envelopeKey,
|
|
485
|
+
stripEnvelope: args.stripEnvelope,
|
|
486
|
+
});
|
|
487
|
+
if (nextRow === change.row_json)
|
|
488
|
+
return change;
|
|
489
|
+
return { ...change, row_json: nextRow };
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
export function createYjsClientPlugin(options) {
|
|
493
|
+
if (options.rules.length === 0) {
|
|
494
|
+
throw new Error('createYjsClientPlugin requires at least one table/field rule');
|
|
495
|
+
}
|
|
496
|
+
const envelopeKey = options.envelopeKey ?? YJS_PAYLOAD_KEY;
|
|
497
|
+
const strict = options.strict ?? true;
|
|
498
|
+
const stripEnvelope = options.stripEnvelope ?? true;
|
|
499
|
+
const stripEnvelopeBeforePush = options.stripEnvelopeBeforePush ?? stripEnvelope;
|
|
500
|
+
const stripEnvelopeBeforeApplyLocalMutations = options.stripEnvelopeBeforeApplyLocalMutations ?? stripEnvelope;
|
|
501
|
+
const maxTrackedRows = Math.max(1, Math.min(1_000_000, options.maxTrackedRows ?? 10_000));
|
|
502
|
+
const { index } = buildRuleIndex(options.rules);
|
|
503
|
+
const stateByRowField = new Map();
|
|
504
|
+
return {
|
|
505
|
+
name: options.name ?? 'crdt-yjs-client',
|
|
506
|
+
priority: options.priority ?? PluginPriority.DEFAULT,
|
|
507
|
+
beforePush(_ctx, request) {
|
|
508
|
+
const nextOperations = request.operations.map((op) => {
|
|
509
|
+
if (op.op !== 'upsert')
|
|
510
|
+
return op;
|
|
511
|
+
if (!isRecord(op.payload))
|
|
512
|
+
return op;
|
|
513
|
+
const nextPayload = transformPushPayload({
|
|
514
|
+
table: op.table,
|
|
515
|
+
rowId: op.row_id,
|
|
516
|
+
payload: op.payload,
|
|
517
|
+
index,
|
|
518
|
+
stateByRowField,
|
|
519
|
+
maxTrackedRows,
|
|
520
|
+
envelopeKey,
|
|
521
|
+
stripEnvelope: stripEnvelopeBeforePush,
|
|
522
|
+
strict,
|
|
523
|
+
});
|
|
524
|
+
if (nextPayload === op.payload)
|
|
525
|
+
return op;
|
|
526
|
+
return { ...op, payload: nextPayload };
|
|
527
|
+
});
|
|
528
|
+
return { ...request, operations: nextOperations };
|
|
529
|
+
},
|
|
530
|
+
beforeApplyLocalMutations(_ctx, args) {
|
|
531
|
+
const nextOperations = args.operations.map((op) => {
|
|
532
|
+
if (op.op === 'delete') {
|
|
533
|
+
clearRowStateCache({
|
|
534
|
+
table: op.table,
|
|
535
|
+
rowId: op.row_id,
|
|
536
|
+
index,
|
|
537
|
+
stateByRowField,
|
|
538
|
+
});
|
|
539
|
+
return op;
|
|
540
|
+
}
|
|
541
|
+
if (op.op !== 'upsert')
|
|
542
|
+
return op;
|
|
543
|
+
if (!isRecord(op.payload))
|
|
544
|
+
return op;
|
|
545
|
+
const nextPayload = transformPushPayload({
|
|
546
|
+
table: op.table,
|
|
547
|
+
rowId: op.row_id,
|
|
548
|
+
payload: op.payload,
|
|
549
|
+
index,
|
|
550
|
+
stateByRowField,
|
|
551
|
+
maxTrackedRows,
|
|
552
|
+
envelopeKey,
|
|
553
|
+
stripEnvelope: stripEnvelopeBeforeApplyLocalMutations,
|
|
554
|
+
strict,
|
|
555
|
+
});
|
|
556
|
+
if (nextPayload === op.payload)
|
|
557
|
+
return op;
|
|
558
|
+
return { ...op, payload: nextPayload };
|
|
559
|
+
});
|
|
560
|
+
return { ...args, operations: nextOperations };
|
|
561
|
+
},
|
|
562
|
+
afterPull(_ctx, args) {
|
|
563
|
+
const nextSubscriptions = args.response.subscriptions.map((sub) => transformPullSubscription({
|
|
564
|
+
sub,
|
|
565
|
+
index,
|
|
566
|
+
stateByRowField,
|
|
567
|
+
maxTrackedRows,
|
|
568
|
+
envelopeKey,
|
|
569
|
+
stripEnvelope,
|
|
570
|
+
}));
|
|
571
|
+
return {
|
|
572
|
+
...args.response,
|
|
573
|
+
subscriptions: nextSubscriptions,
|
|
574
|
+
};
|
|
575
|
+
},
|
|
576
|
+
beforeApplyWsChanges(_ctx, args) {
|
|
577
|
+
const nextChanges = transformWsChanges({
|
|
578
|
+
changes: args.changes,
|
|
579
|
+
index,
|
|
580
|
+
stateByRowField,
|
|
581
|
+
maxTrackedRows,
|
|
582
|
+
envelopeKey,
|
|
583
|
+
stripEnvelope,
|
|
584
|
+
});
|
|
585
|
+
return {
|
|
586
|
+
...args,
|
|
587
|
+
changes: nextChanges,
|
|
588
|
+
};
|
|
589
|
+
},
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
//# 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/client-plugin-yjs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6-159",
|
|
4
4
|
"description": "Yjs CRDT plugin primitives for Syncular client integration",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"release": "bunx syncular-publish"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@syncular/client": "0.0.
|
|
47
|
+
"@syncular/client": "0.0.6-159",
|
|
48
48
|
"yjs": "^13.6.29"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
package/src/index.test.ts
CHANGED
|
@@ -317,6 +317,189 @@ describe('@syncular/client-plugin-crdt-yjs', () => {
|
|
|
317
317
|
expect(typeof payload.content_yjs_state).toBe('string');
|
|
318
318
|
});
|
|
319
319
|
|
|
320
|
+
it('invalidates cached row state after pull commit delete before row-id reuse', async () => {
|
|
321
|
+
const plugin = createYjsClientPlugin({
|
|
322
|
+
rules: [
|
|
323
|
+
{
|
|
324
|
+
table: 'tasks',
|
|
325
|
+
field: 'content',
|
|
326
|
+
stateColumn: 'content_yjs_state',
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const old = createUpdate('old');
|
|
332
|
+
await callAfterPull(plugin, {
|
|
333
|
+
ok: true,
|
|
334
|
+
subscriptions: [
|
|
335
|
+
{
|
|
336
|
+
id: 'tasks',
|
|
337
|
+
status: 'active',
|
|
338
|
+
scopes: {},
|
|
339
|
+
bootstrap: true,
|
|
340
|
+
nextCursor: 1,
|
|
341
|
+
bootstrapState: null,
|
|
342
|
+
commits: [],
|
|
343
|
+
snapshots: [
|
|
344
|
+
{
|
|
345
|
+
table: 'tasks',
|
|
346
|
+
rows: [
|
|
347
|
+
{
|
|
348
|
+
id: 'task-1',
|
|
349
|
+
content: 'stale',
|
|
350
|
+
content_yjs_state: old.state,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
isFirstPage: true,
|
|
354
|
+
isLastPage: true,
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await callAfterPull(plugin, {
|
|
362
|
+
ok: true,
|
|
363
|
+
subscriptions: [
|
|
364
|
+
{
|
|
365
|
+
id: 'tasks',
|
|
366
|
+
status: 'active',
|
|
367
|
+
scopes: {},
|
|
368
|
+
bootstrap: false,
|
|
369
|
+
nextCursor: 2,
|
|
370
|
+
bootstrapState: null,
|
|
371
|
+
snapshots: [],
|
|
372
|
+
commits: [
|
|
373
|
+
{
|
|
374
|
+
commitSeq: 2,
|
|
375
|
+
createdAt: '2026-03-01T00:00:00.000Z',
|
|
376
|
+
actorId: 'actor-2',
|
|
377
|
+
changes: [
|
|
378
|
+
{
|
|
379
|
+
table: 'tasks',
|
|
380
|
+
row_id: 'task-1',
|
|
381
|
+
op: 'delete',
|
|
382
|
+
row_json: null,
|
|
383
|
+
row_version: null,
|
|
384
|
+
scopes: {},
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const fresh = createUpdate('new');
|
|
394
|
+
const pushed = await callBeforePush(plugin, {
|
|
395
|
+
clientId: 'client-1',
|
|
396
|
+
clientCommitId: 'commit-reuse-after-delete',
|
|
397
|
+
schemaVersion: 1,
|
|
398
|
+
operations: [
|
|
399
|
+
{
|
|
400
|
+
table: 'tasks',
|
|
401
|
+
row_id: 'task-1',
|
|
402
|
+
op: 'upsert',
|
|
403
|
+
payload: {
|
|
404
|
+
[YJS_PAYLOAD_KEY]: {
|
|
405
|
+
content: fresh.update,
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
base_version: null,
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const payload = pushed.operations[0]?.payload as
|
|
414
|
+
| Record<string, unknown>
|
|
415
|
+
| undefined;
|
|
416
|
+
if (!payload) {
|
|
417
|
+
throw new Error('Expected transformed push payload');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
expect(payload.content).toBe('new');
|
|
421
|
+
expect(String(payload.content)).not.toContain('old');
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('evicts least-recently-used cached row state when maxTrackedRows is reached', async () => {
|
|
425
|
+
const plugin = createYjsClientPlugin({
|
|
426
|
+
rules: [
|
|
427
|
+
{
|
|
428
|
+
table: 'tasks',
|
|
429
|
+
field: 'content',
|
|
430
|
+
stateColumn: 'content_yjs_state',
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
maxTrackedRows: 1,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const rowOne = createUpdate('old');
|
|
437
|
+
const rowTwo = createUpdate('other');
|
|
438
|
+
|
|
439
|
+
await callAfterPull(plugin, {
|
|
440
|
+
ok: true,
|
|
441
|
+
subscriptions: [
|
|
442
|
+
{
|
|
443
|
+
id: 'tasks',
|
|
444
|
+
status: 'active',
|
|
445
|
+
scopes: {},
|
|
446
|
+
bootstrap: true,
|
|
447
|
+
nextCursor: 1,
|
|
448
|
+
bootstrapState: null,
|
|
449
|
+
commits: [],
|
|
450
|
+
snapshots: [
|
|
451
|
+
{
|
|
452
|
+
table: 'tasks',
|
|
453
|
+
rows: [
|
|
454
|
+
{
|
|
455
|
+
id: 'task-1',
|
|
456
|
+
content: 'stale',
|
|
457
|
+
content_yjs_state: rowOne.state,
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
id: 'task-2',
|
|
461
|
+
content: 'stale',
|
|
462
|
+
content_yjs_state: rowTwo.state,
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
isFirstPage: true,
|
|
466
|
+
isLastPage: true,
|
|
467
|
+
},
|
|
468
|
+
],
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const fullFromEmpty = createUpdate('new');
|
|
474
|
+
const pushed = await callBeforePush(plugin, {
|
|
475
|
+
clientId: 'client-1',
|
|
476
|
+
clientCommitId: 'commit-eviction',
|
|
477
|
+
schemaVersion: 1,
|
|
478
|
+
operations: [
|
|
479
|
+
{
|
|
480
|
+
table: 'tasks',
|
|
481
|
+
row_id: 'task-1',
|
|
482
|
+
op: 'upsert',
|
|
483
|
+
payload: {
|
|
484
|
+
[YJS_PAYLOAD_KEY]: {
|
|
485
|
+
content: fullFromEmpty.update,
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
base_version: null,
|
|
489
|
+
},
|
|
490
|
+
],
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const payload = pushed.operations[0]?.payload as
|
|
494
|
+
| Record<string, unknown>
|
|
495
|
+
| undefined;
|
|
496
|
+
if (!payload) {
|
|
497
|
+
throw new Error('Expected transformed push payload');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
expect(payload.content).toBe('new');
|
|
501
|
+
});
|
|
502
|
+
|
|
320
503
|
it('materializes websocket inline changes through beforeApplyWsChanges', async () => {
|
|
321
504
|
const plugin = createYjsClientPlugin({
|
|
322
505
|
rules: [
|
package/src/index.ts
CHANGED
|
@@ -87,6 +87,12 @@ export interface CreateYjsClientPluginOptions {
|
|
|
87
87
|
rules: readonly YjsClientFieldRule[];
|
|
88
88
|
envelopeKey?: string;
|
|
89
89
|
priority?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Maximum cached row-field Yjs states to keep in memory.
|
|
92
|
+
* Prevents unbounded growth in long-lived sessions.
|
|
93
|
+
* @default 10_000
|
|
94
|
+
*/
|
|
95
|
+
maxTrackedRows?: number;
|
|
90
96
|
/**
|
|
91
97
|
* Throw when envelope payload references fields without matching rules.
|
|
92
98
|
* @default true
|
|
@@ -323,6 +329,36 @@ function rowFieldCacheKey(table: string, rowId: string, field: string): string {
|
|
|
323
329
|
return `${table}\u001f${rowId}\u001f${field}`;
|
|
324
330
|
}
|
|
325
331
|
|
|
332
|
+
function touchLruState(
|
|
333
|
+
map: Map<string, string>,
|
|
334
|
+
key: string,
|
|
335
|
+
value: string,
|
|
336
|
+
maxTrackedRows: number
|
|
337
|
+
): void {
|
|
338
|
+
map.delete(key);
|
|
339
|
+
map.set(key, value);
|
|
340
|
+
if (map.size <= maxTrackedRows) return;
|
|
341
|
+
const oldest = map.keys().next().value as string | undefined;
|
|
342
|
+
if (oldest) {
|
|
343
|
+
map.delete(oldest);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function clearRowStateCache(args: {
|
|
348
|
+
table: string;
|
|
349
|
+
rowId: string;
|
|
350
|
+
index: RuleIndex;
|
|
351
|
+
stateByRowField: Map<string, string>;
|
|
352
|
+
}): void {
|
|
353
|
+
const tableRules = args.index.get(args.table);
|
|
354
|
+
if (!tableRules) return;
|
|
355
|
+
for (const rule of tableRules.values()) {
|
|
356
|
+
args.stateByRowField.delete(
|
|
357
|
+
rowFieldCacheKey(args.table, args.rowId, rule.field)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
326
362
|
function resolveSnapshotRowId(
|
|
327
363
|
row: Record<string, unknown>,
|
|
328
364
|
rule: ResolvedYjsClientFieldRule
|
|
@@ -423,6 +459,7 @@ function materializeRowFromState(args: {
|
|
|
423
459
|
row: Record<string, unknown>;
|
|
424
460
|
index: RuleIndex;
|
|
425
461
|
stateByRowField: Map<string, string>;
|
|
462
|
+
maxTrackedRows: number;
|
|
426
463
|
envelopeKey: string;
|
|
427
464
|
stripEnvelope: boolean;
|
|
428
465
|
}): Record<string, unknown> {
|
|
@@ -455,9 +492,11 @@ function materializeRowFromState(args: {
|
|
|
455
492
|
ensureRow()[rule.field] = nextValue;
|
|
456
493
|
}
|
|
457
494
|
if (args.rowId) {
|
|
458
|
-
|
|
495
|
+
touchLruState(
|
|
496
|
+
args.stateByRowField,
|
|
459
497
|
rowFieldCacheKey(args.table, args.rowId, rule.field),
|
|
460
|
-
stateBase64
|
|
498
|
+
stateBase64,
|
|
499
|
+
args.maxTrackedRows
|
|
461
500
|
);
|
|
462
501
|
}
|
|
463
502
|
} finally {
|
|
@@ -482,6 +521,7 @@ function transformPushPayload(args: {
|
|
|
482
521
|
payload: Record<string, unknown>;
|
|
483
522
|
index: RuleIndex;
|
|
484
523
|
stateByRowField: Map<string, string>;
|
|
524
|
+
maxTrackedRows: number;
|
|
485
525
|
envelopeKey: string;
|
|
486
526
|
stripEnvelope: boolean;
|
|
487
527
|
strict: boolean;
|
|
@@ -521,9 +561,11 @@ function transformPushPayload(args: {
|
|
|
521
561
|
const source = nextPayload ?? args.payload;
|
|
522
562
|
const stateBase64 = stateValueToBase64(source[rule.stateColumn]);
|
|
523
563
|
if (stateBase64) {
|
|
524
|
-
|
|
564
|
+
touchLruState(
|
|
565
|
+
args.stateByRowField,
|
|
525
566
|
rowFieldCacheKey(args.table, args.rowId, rule.field),
|
|
526
|
-
stateBase64
|
|
567
|
+
stateBase64,
|
|
568
|
+
args.maxTrackedRows
|
|
527
569
|
);
|
|
528
570
|
}
|
|
529
571
|
}
|
|
@@ -566,7 +608,12 @@ function transformPushPayload(args: {
|
|
|
566
608
|
const target = ensurePayload();
|
|
567
609
|
target[rule.field] = nextValue;
|
|
568
610
|
target[rule.stateColumn] = nextStateBase64;
|
|
569
|
-
|
|
611
|
+
touchLruState(
|
|
612
|
+
args.stateByRowField,
|
|
613
|
+
cacheKey,
|
|
614
|
+
nextStateBase64,
|
|
615
|
+
args.maxTrackedRows
|
|
616
|
+
);
|
|
570
617
|
} finally {
|
|
571
618
|
doc.destroy();
|
|
572
619
|
}
|
|
@@ -588,6 +635,7 @@ function transformPullSubscription(args: {
|
|
|
588
635
|
sub: SyncPullSubscriptionResponse;
|
|
589
636
|
index: RuleIndex;
|
|
590
637
|
stateByRowField: Map<string, string>;
|
|
638
|
+
maxTrackedRows: number;
|
|
591
639
|
envelopeKey: string;
|
|
592
640
|
stripEnvelope: boolean;
|
|
593
641
|
}): SyncPullSubscriptionResponse {
|
|
@@ -603,6 +651,7 @@ function transformPullSubscription(args: {
|
|
|
603
651
|
row,
|
|
604
652
|
index: args.index,
|
|
605
653
|
stateByRowField: args.stateByRowField,
|
|
654
|
+
maxTrackedRows: args.maxTrackedRows,
|
|
606
655
|
envelopeKey: args.envelopeKey,
|
|
607
656
|
stripEnvelope: args.stripEnvelope,
|
|
608
657
|
});
|
|
@@ -620,6 +669,7 @@ function transformPullSubscription(args: {
|
|
|
620
669
|
row,
|
|
621
670
|
index: args.index,
|
|
622
671
|
stateByRowField: args.stateByRowField,
|
|
672
|
+
maxTrackedRows: args.maxTrackedRows,
|
|
623
673
|
envelopeKey: args.envelopeKey,
|
|
624
674
|
stripEnvelope: args.stripEnvelope,
|
|
625
675
|
});
|
|
@@ -629,6 +679,15 @@ function transformPullSubscription(args: {
|
|
|
629
679
|
const nextCommits = (args.sub.commits ?? []).map((commit) => ({
|
|
630
680
|
...commit,
|
|
631
681
|
changes: (commit.changes ?? []).map((change) => {
|
|
682
|
+
if (change.op === 'delete') {
|
|
683
|
+
clearRowStateCache({
|
|
684
|
+
table: change.table,
|
|
685
|
+
rowId: change.row_id,
|
|
686
|
+
index: args.index,
|
|
687
|
+
stateByRowField: args.stateByRowField,
|
|
688
|
+
});
|
|
689
|
+
return change;
|
|
690
|
+
}
|
|
632
691
|
if (change.op !== 'upsert' || !isRecord(change.row_json)) return change;
|
|
633
692
|
const nextRow = materializeRowFromState({
|
|
634
693
|
table: change.table,
|
|
@@ -636,6 +695,7 @@ function transformPullSubscription(args: {
|
|
|
636
695
|
row: change.row_json,
|
|
637
696
|
index: args.index,
|
|
638
697
|
stateByRowField: args.stateByRowField,
|
|
698
|
+
maxTrackedRows: args.maxTrackedRows,
|
|
639
699
|
envelopeKey: args.envelopeKey,
|
|
640
700
|
stripEnvelope: args.stripEnvelope,
|
|
641
701
|
});
|
|
@@ -655,10 +715,20 @@ function transformWsChanges(args: {
|
|
|
655
715
|
changes: SyncChange[];
|
|
656
716
|
index: RuleIndex;
|
|
657
717
|
stateByRowField: Map<string, string>;
|
|
718
|
+
maxTrackedRows: number;
|
|
658
719
|
envelopeKey: string;
|
|
659
720
|
stripEnvelope: boolean;
|
|
660
721
|
}): SyncChange[] {
|
|
661
722
|
return args.changes.map((change) => {
|
|
723
|
+
if (change.op === 'delete') {
|
|
724
|
+
clearRowStateCache({
|
|
725
|
+
table: change.table,
|
|
726
|
+
rowId: change.row_id,
|
|
727
|
+
index: args.index,
|
|
728
|
+
stateByRowField: args.stateByRowField,
|
|
729
|
+
});
|
|
730
|
+
return change;
|
|
731
|
+
}
|
|
662
732
|
if (change.op !== 'upsert' || !isRecord(change.row_json)) return change;
|
|
663
733
|
const nextRow = materializeRowFromState({
|
|
664
734
|
table: change.table,
|
|
@@ -666,6 +736,7 @@ function transformWsChanges(args: {
|
|
|
666
736
|
row: change.row_json,
|
|
667
737
|
index: args.index,
|
|
668
738
|
stateByRowField: args.stateByRowField,
|
|
739
|
+
maxTrackedRows: args.maxTrackedRows,
|
|
669
740
|
envelopeKey: args.envelopeKey,
|
|
670
741
|
stripEnvelope: args.stripEnvelope,
|
|
671
742
|
});
|
|
@@ -690,6 +761,10 @@ export function createYjsClientPlugin(
|
|
|
690
761
|
options.stripEnvelopeBeforePush ?? stripEnvelope;
|
|
691
762
|
const stripEnvelopeBeforeApplyLocalMutations =
|
|
692
763
|
options.stripEnvelopeBeforeApplyLocalMutations ?? stripEnvelope;
|
|
764
|
+
const maxTrackedRows = Math.max(
|
|
765
|
+
1,
|
|
766
|
+
Math.min(1_000_000, options.maxTrackedRows ?? 10_000)
|
|
767
|
+
);
|
|
693
768
|
const { index } = buildRuleIndex(options.rules);
|
|
694
769
|
const stateByRowField = new Map<string, string>();
|
|
695
770
|
|
|
@@ -708,6 +783,7 @@ export function createYjsClientPlugin(
|
|
|
708
783
|
payload: op.payload,
|
|
709
784
|
index,
|
|
710
785
|
stateByRowField,
|
|
786
|
+
maxTrackedRows,
|
|
711
787
|
envelopeKey,
|
|
712
788
|
stripEnvelope: stripEnvelopeBeforePush,
|
|
713
789
|
strict,
|
|
@@ -725,6 +801,15 @@ export function createYjsClientPlugin(
|
|
|
725
801
|
args: SyncClientLocalMutationArgs
|
|
726
802
|
): SyncClientLocalMutationArgs {
|
|
727
803
|
const nextOperations = args.operations.map((op) => {
|
|
804
|
+
if (op.op === 'delete') {
|
|
805
|
+
clearRowStateCache({
|
|
806
|
+
table: op.table,
|
|
807
|
+
rowId: op.row_id,
|
|
808
|
+
index,
|
|
809
|
+
stateByRowField,
|
|
810
|
+
});
|
|
811
|
+
return op;
|
|
812
|
+
}
|
|
728
813
|
if (op.op !== 'upsert') return op;
|
|
729
814
|
if (!isRecord(op.payload)) return op;
|
|
730
815
|
|
|
@@ -734,6 +819,7 @@ export function createYjsClientPlugin(
|
|
|
734
819
|
payload: op.payload,
|
|
735
820
|
index,
|
|
736
821
|
stateByRowField,
|
|
822
|
+
maxTrackedRows,
|
|
737
823
|
envelopeKey,
|
|
738
824
|
stripEnvelope: stripEnvelopeBeforeApplyLocalMutations,
|
|
739
825
|
strict,
|
|
@@ -752,6 +838,7 @@ export function createYjsClientPlugin(
|
|
|
752
838
|
sub,
|
|
753
839
|
index,
|
|
754
840
|
stateByRowField,
|
|
841
|
+
maxTrackedRows,
|
|
755
842
|
envelopeKey,
|
|
756
843
|
stripEnvelope,
|
|
757
844
|
})
|
|
@@ -771,6 +858,7 @@ export function createYjsClientPlugin(
|
|
|
771
858
|
changes: args.changes,
|
|
772
859
|
index,
|
|
773
860
|
stateByRowField,
|
|
861
|
+
maxTrackedRows,
|
|
774
862
|
envelopeKey,
|
|
775
863
|
stripEnvelope,
|
|
776
864
|
});
|