@korajs/sync 1.0.0-beta.11 → 1.0.0-beta.13
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 +30 -40
- package/dist/chunk-5S6U7EVU.js +344 -0
- package/dist/chunk-5S6U7EVU.js.map +1 -0
- package/dist/index.cjs +6671 -1082
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1625 -96
- package/dist/index.d.ts +1625 -96
- package/dist/index.js +6382 -1100
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +313 -2
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +117 -3
- package/dist/internal.d.ts +117 -3
- package/dist/internal.js +17 -3
- package/dist/internal.js.map +1 -1
- package/dist/transport-Bemtqwep.d.cts +1448 -0
- package/dist/transport-Bemtqwep.d.ts +1448 -0
- package/package.json +3 -3
- package/dist/chunk-TU345YUC.js +0 -23
- package/dist/chunk-TU345YUC.js.map +0 -1
- package/dist/transport-iXX0KA_W.d.cts +0 -727
- package/dist/transport-iXX0KA_W.d.ts +0 -727
package/README.md
CHANGED
|
@@ -1,67 +1,57 @@
|
|
|
1
1
|
# @korajs/sync
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The sync engine of Kora.js: protocol v2 over WebSocket or HTTP long-polling, an outbound queue that
|
|
4
|
+
survives reloads, gap-free resumable downloads, reconnection with backoff, end-to-end encryption
|
|
5
|
+
and presence.
|
|
4
6
|
|
|
5
|
-
> Most
|
|
7
|
+
> Most apps do not install this directly: `createApp({ sync: { url } })` from
|
|
8
|
+
> [`korajs`](https://www.npmjs.com/package/korajs) creates and runs the engine.
|
|
6
9
|
|
|
7
10
|
## Install
|
|
8
11
|
|
|
9
12
|
```bash
|
|
10
|
-
pnpm add @korajs/sync
|
|
13
|
+
pnpm add @korajs/sync@beta
|
|
11
14
|
```
|
|
12
15
|
|
|
13
16
|
## Usage
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
<!-- docs-check: standalone -->
|
|
17
19
|
```typescript
|
|
18
|
-
import {
|
|
20
|
+
import { createApp, defineSchema, t } from 'korajs'
|
|
19
21
|
|
|
20
|
-
const
|
|
21
|
-
url: 'wss://my-server.com/kora',
|
|
22
|
-
auth: async () => ({ token: await getAuthToken() }),
|
|
23
|
-
})
|
|
22
|
+
const schema = defineSchema({ version: 1, collections: { todos: { fields: { title: t.string() } } } })
|
|
24
23
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
operationLog,
|
|
24
|
+
const app = createApp({
|
|
25
|
+
schema,
|
|
26
|
+
sync: { url: 'wss://sync.example.com/kora-sync', autoConnect: true },
|
|
29
27
|
})
|
|
30
28
|
|
|
31
|
-
sync
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
### HTTP Transport
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
import { HttpTransport } from '@korajs/sync'
|
|
38
|
-
|
|
39
|
-
const transport = new HttpTransport({
|
|
40
|
-
url: 'https://my-server.com/kora/sync',
|
|
41
|
-
pollInterval: 5000,
|
|
29
|
+
app.sync?.subscribeStatus((status) => {
|
|
30
|
+
console.log(status.status, status.pendingOperations)
|
|
42
31
|
})
|
|
32
|
+
app.events.on('sync:operation-rejected', (event) => console.warn(event.code, event.message))
|
|
43
33
|
```
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
sync.on('connected', () => console.log('Connected'))
|
|
49
|
-
sync.on('disconnected', (reason) => console.log('Disconnected:', reason))
|
|
50
|
-
sync.on('sent', (ops) => console.log('Sent', ops.length, 'operations'))
|
|
51
|
-
sync.on('received', (ops) => console.log('Received', ops.length, 'operations'))
|
|
52
|
-
```
|
|
35
|
+
`SyncEngine`, `WebSocketTransport`, `HttpLongPollingTransport`, `ChaosTransport`, the protocol
|
|
36
|
+
message types and the encryption keyring are exported for custom runtimes and tests.
|
|
53
37
|
|
|
54
38
|
## Protocol
|
|
55
39
|
|
|
56
|
-
1. **Handshake
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
40
|
+
1. **Handshake**: protocol version, schema version, the client's version vector and scope; the
|
|
41
|
+
server answers with its grant, its time (clock skew) and the delivery position to resume from.
|
|
42
|
+
2. **Upload**: the operations the server has not acknowledged, in causal order.
|
|
43
|
+
3. **Download**: batches chained by server delivery sequence. The client applies a batch only when
|
|
44
|
+
it continues its durable watermark, so a dropped or failed operation is re-sent, never skipped.
|
|
45
|
+
4. **Streaming**: both directions continue in real time; heartbeats detect dead connections.
|
|
60
46
|
|
|
61
|
-
Operations are
|
|
47
|
+
Operations are content-addressed, so a duplicate is a no-op. Messages are JSON.
|
|
48
|
+
|
|
49
|
+
## Documentation
|
|
50
|
+
|
|
51
|
+
[Sync Configuration](https://korajs.dev/guide/sync-configuration),
|
|
52
|
+
[Sync Protocol](https://korajs.dev/guide/sync-protocol) and the
|
|
53
|
+
[Sync API reference](https://korajs.dev/api/sync).
|
|
62
54
|
|
|
63
55
|
## License
|
|
64
56
|
|
|
65
57
|
MIT
|
|
66
|
-
|
|
67
|
-
See the [full documentation](https://github.com/ehoneahobed/kora) for guides, API reference, and examples.
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// src/scopes/scope-snapshot.ts
|
|
2
|
+
function buildScopeSnapshot(op, fullRecord, options = {}) {
|
|
3
|
+
return {
|
|
4
|
+
...fullRecord ?? {},
|
|
5
|
+
...options.includePreviousData ? asPlainRecord(op.previousData) ?? {} : {},
|
|
6
|
+
...asPlainRecord(op.data) ?? {},
|
|
7
|
+
id: op.recordId
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function matchesScopePredicate(actual, expected) {
|
|
11
|
+
if (expected === void 0 || expected === null) return false;
|
|
12
|
+
if (typeof expected === "object" && !Array.isArray(expected) && "$in" in expected) {
|
|
13
|
+
const values = expected.$in;
|
|
14
|
+
return Array.isArray(values) && values.some((value) => value !== void 0 && value !== null && Object.is(actual, value));
|
|
15
|
+
}
|
|
16
|
+
return Object.is(actual, expected);
|
|
17
|
+
}
|
|
18
|
+
function recordMatchesScopePredicates(record, collectionScope) {
|
|
19
|
+
for (const [field, expected] of Object.entries(collectionScope)) {
|
|
20
|
+
if (!matchesScopePredicate(record[field], expected)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
function asPlainRecord(value) {
|
|
27
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/protocol/protocol-version.ts
|
|
34
|
+
var SYNC_PROTOCOL_VERSION = 2;
|
|
35
|
+
var LEGACY_SYNC_PROTOCOL_VERSION = 1;
|
|
36
|
+
var PROTOCOL_V1_DEPRECATED = "PROTOCOL_V1_DEPRECATED";
|
|
37
|
+
var INVALID_OPERATION_ID = "INVALID_OPERATION_ID";
|
|
38
|
+
var PLAINTEXT_REJECTED = "PLAINTEXT_REJECTED";
|
|
39
|
+
function declaredProtocolVersion(declared) {
|
|
40
|
+
return typeof declared === "number" && Number.isInteger(declared) && declared >= 1 ? declared : LEGACY_SYNC_PROTOCOL_VERSION;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/scopes/scope-view-key.ts
|
|
44
|
+
function scopeViewKey(scope) {
|
|
45
|
+
if (scope === null || scope === void 0) return "";
|
|
46
|
+
return canonicalJson(normalizeValue(scope));
|
|
47
|
+
}
|
|
48
|
+
function normalizeValue(value) {
|
|
49
|
+
if (Array.isArray(value)) return value.map(normalizeValue);
|
|
50
|
+
if (value === null || typeof value !== "object") return value;
|
|
51
|
+
const record = value;
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const key of Object.keys(record)) {
|
|
54
|
+
const entry = record[key];
|
|
55
|
+
if (entry === void 0) continue;
|
|
56
|
+
if (key === "$in" && Array.isArray(entry)) {
|
|
57
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const item of entry) {
|
|
59
|
+
const normalized = normalizeValue(item);
|
|
60
|
+
byKey.set(canonicalJson(normalized), normalized);
|
|
61
|
+
}
|
|
62
|
+
out[key] = [...byKey.keys()].sort().map((k) => byKey.get(k));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
out[key] = normalizeValue(entry);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function canonicalJson(value) {
|
|
70
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
71
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
72
|
+
const record = value;
|
|
73
|
+
const entries = Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`);
|
|
74
|
+
return `{${entries.join(",")}}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/engine/memory-queue-storage.ts
|
|
78
|
+
var MemoryQueueStorage = class {
|
|
79
|
+
operations = /* @__PURE__ */ new Map();
|
|
80
|
+
sent = /* @__PURE__ */ new Set();
|
|
81
|
+
async load() {
|
|
82
|
+
return [...this.operations.values()];
|
|
83
|
+
}
|
|
84
|
+
async enqueue(op) {
|
|
85
|
+
this.operations.set(op.id, op);
|
|
86
|
+
}
|
|
87
|
+
async dequeue(ids) {
|
|
88
|
+
for (const id of ids) {
|
|
89
|
+
this.operations.delete(id);
|
|
90
|
+
this.sent.delete(id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async markSent(ops) {
|
|
94
|
+
for (const op of ops) {
|
|
95
|
+
if (this.operations.has(op.id)) this.sent.add(op.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async loadSentIds() {
|
|
99
|
+
return [...this.sent];
|
|
100
|
+
}
|
|
101
|
+
async count() {
|
|
102
|
+
return this.operations.size;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/engine/verify-inbound.ts
|
|
107
|
+
import {
|
|
108
|
+
base64ToBytes,
|
|
109
|
+
bytesToBase64,
|
|
110
|
+
canonicalizeLegacyOperation,
|
|
111
|
+
isKoraBytesValue,
|
|
112
|
+
verifyOperationId
|
|
113
|
+
} from "@korajs/core";
|
|
114
|
+
var RESERVED_NODE_PREFIX = "kora:";
|
|
115
|
+
var SCOPE_ENTRY_NODE_ID = "kora:scope-entry";
|
|
116
|
+
function isExemptFromIdVerification(op) {
|
|
117
|
+
if (!op.nodeId.startsWith(RESERVED_NODE_PREFIX)) return false;
|
|
118
|
+
return op.nodeId === SCOPE_ENTRY_NODE_ID || op.hashVersion === void 0;
|
|
119
|
+
}
|
|
120
|
+
async function verifyInboundOperation(op, context) {
|
|
121
|
+
if (isExemptFromIdVerification(op)) return { ok: true, verified: false };
|
|
122
|
+
const declared = op.hashVersion;
|
|
123
|
+
if (declared !== void 0 && declared !== 1 && declared !== 2) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
code: INVALID_OPERATION_ID,
|
|
127
|
+
message: `Operation "${op.id}" declares unknown content-hash version ${String(declared)}; it cannot be verified and is not applied. Upgrade this client.`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (!context.encrypted && declared === void 0 && (context.absentVersion ?? "skip") === "skip") {
|
|
131
|
+
return { ok: true, verified: false };
|
|
132
|
+
}
|
|
133
|
+
if (declared === void 0 && !context.encrypted) {
|
|
134
|
+
if (await verifyOperationId(op)) return { ok: true, verified: true, matchedVersion: 1 };
|
|
135
|
+
if (await verifyOperationId({ ...op, hashVersion: 2 })) {
|
|
136
|
+
return { ok: true, verified: true, matchedVersion: 2 };
|
|
137
|
+
}
|
|
138
|
+
const match = await matchOperationId(op, context.schema ?? null);
|
|
139
|
+
if (match !== null) {
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
verified: true,
|
|
143
|
+
matchedVersion: 1,
|
|
144
|
+
// Not declarable: a receiver without the schema cannot repeat a nested rebuild,
|
|
145
|
+
// and a Date-form match never bound the Date's value (RT-90), so the stored
|
|
146
|
+
// copy must not claim a verified version.
|
|
147
|
+
declarable: !match.usedSchema && match.usedDateForm !== true,
|
|
148
|
+
...match.usedPrevious ? { restoredData: restoreUndefinedFromPrevious(op) } : {}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
} else if (await matchOperationId(op, context.schema ?? null) !== null) {
|
|
152
|
+
return { ok: true, verified: true };
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
code: INVALID_OPERATION_ID,
|
|
157
|
+
message: `Operation "${op.id}" from node "${op.nodeId}" does not match its content hash (hash version ${String(declared ?? 1)}${context.encrypted ? ", after decryption" : ""}): it was altered after it was created, or its id was not computed from its content. It is refused, not applied.`
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function operationIdMatches(op, schema = null) {
|
|
161
|
+
return await matchOperationId(op, schema) !== null;
|
|
162
|
+
}
|
|
163
|
+
async function matchOperationId(op, schema) {
|
|
164
|
+
if (await verifyOperationId(op)) return { usedPrevious: false, usedSchema: false };
|
|
165
|
+
if ((op.hashVersion ?? 1) !== 1) return null;
|
|
166
|
+
const candidates = [
|
|
167
|
+
{ data: op.data, kind: { usedPrevious: false, usedSchema: false } }
|
|
168
|
+
];
|
|
169
|
+
const filled = restoreUndefinedFromPrevious(op);
|
|
170
|
+
if (filled !== op.data) {
|
|
171
|
+
candidates.push({ data: filled, kind: { usedPrevious: true, usedSchema: false } });
|
|
172
|
+
}
|
|
173
|
+
if (schema) {
|
|
174
|
+
for (const candidate of [...candidates]) {
|
|
175
|
+
const nested = fillDeclaredNestedMembers(candidate.data, op.collection, schema);
|
|
176
|
+
if (nested !== null) {
|
|
177
|
+
candidates.push({ data: nested, kind: { ...candidate.kind, usedSchema: true } });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const candidate of [...candidates]) {
|
|
182
|
+
for (const data of dateFormCandidates(candidate.data)) {
|
|
183
|
+
candidates.push({ data, kind: { ...candidate.kind, usedDateForm: true } });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const candidate of candidates) {
|
|
187
|
+
if (candidate.data !== op.data && await verifyOperationId({ ...op, data: candidate.data })) {
|
|
188
|
+
return candidate.kind;
|
|
189
|
+
}
|
|
190
|
+
for (const convert of [toBytesForm, toKoraBytesForm]) {
|
|
191
|
+
const data = convert(candidate.data);
|
|
192
|
+
const atomicOps = op.atomicOps === void 0 ? void 0 : convert(op.atomicOps);
|
|
193
|
+
if (data.changed || atomicOps?.changed) {
|
|
194
|
+
const variant = {
|
|
195
|
+
...op,
|
|
196
|
+
data: data.value,
|
|
197
|
+
...atomicOps ? { atomicOps: atomicOps.value } : {}
|
|
198
|
+
};
|
|
199
|
+
if (await verifyOperationId(variant)) return candidate.kind;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
function restoreUndefinedFromPrevious(op) {
|
|
206
|
+
return canonicalizeLegacyOperation({ ...op, hashVersion: void 0 }).data;
|
|
207
|
+
}
|
|
208
|
+
function fillDeclaredNestedMembers(data, collection, schema) {
|
|
209
|
+
const fields = schema.collections[collection]?.fields;
|
|
210
|
+
if (data === null || fields === void 0) return null;
|
|
211
|
+
let changed = false;
|
|
212
|
+
const out = { ...data };
|
|
213
|
+
for (const [name, value] of Object.entries(data)) {
|
|
214
|
+
const descriptor = fields[name];
|
|
215
|
+
if (descriptor === void 0) continue;
|
|
216
|
+
const filled = fillNested(value, descriptor);
|
|
217
|
+
if (filled !== value) {
|
|
218
|
+
out[name] = filled;
|
|
219
|
+
changed = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return changed ? out : null;
|
|
223
|
+
}
|
|
224
|
+
function fillNested(value, descriptor) {
|
|
225
|
+
const nested = descriptor.nestedFields;
|
|
226
|
+
if (descriptor.kind !== "object" || !nested) return value;
|
|
227
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
|
228
|
+
const record = value;
|
|
229
|
+
let changed = false;
|
|
230
|
+
const out = { ...record };
|
|
231
|
+
for (const [name, child] of Object.entries(nested)) {
|
|
232
|
+
if (!(name in record)) {
|
|
233
|
+
out[name] = null;
|
|
234
|
+
changed = true;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const filled = fillNested(record[name], child);
|
|
238
|
+
if (filled !== record[name]) {
|
|
239
|
+
out[name] = filled;
|
|
240
|
+
changed = true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return changed ? out : value;
|
|
244
|
+
}
|
|
245
|
+
var ISO_DATE_STRING = /^(?:\d{4}|[+-]\d{6})-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
246
|
+
var MAX_DATE_FORM_STRINGS = 6;
|
|
247
|
+
function dateFormCandidates(data) {
|
|
248
|
+
if (data === null) return [];
|
|
249
|
+
const paths = [];
|
|
250
|
+
const collect = (value, path) => {
|
|
251
|
+
if (typeof value === "string") {
|
|
252
|
+
if (ISO_DATE_STRING.test(value)) paths.push(path);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (Array.isArray(value)) {
|
|
256
|
+
value.forEach((item, index) => collect(item, [...path, index]));
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (value !== null && typeof value === "object" && !(value instanceof Uint8Array)) {
|
|
260
|
+
for (const [key, item] of Object.entries(value)) collect(item, [...path, key]);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
for (const [field, value] of Object.entries(data)) collect(value, [field]);
|
|
264
|
+
if (paths.length === 0) return [];
|
|
265
|
+
const subsets = [];
|
|
266
|
+
if (paths.length > MAX_DATE_FORM_STRINGS) {
|
|
267
|
+
subsets.push(paths);
|
|
268
|
+
} else {
|
|
269
|
+
for (let mask = 1; mask < 1 << paths.length; mask++) {
|
|
270
|
+
subsets.push(paths.filter((_path, index) => (mask & 1 << index) !== 0));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return subsets.map((subset) => {
|
|
274
|
+
let out = data;
|
|
275
|
+
for (const path of subset) out = replaceAt(out, path, {});
|
|
276
|
+
return out;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function replaceAt(value, path, replacement) {
|
|
280
|
+
const [head, ...rest] = path;
|
|
281
|
+
if (head === void 0) return replacement;
|
|
282
|
+
if (Array.isArray(value)) {
|
|
283
|
+
const copy = [...value];
|
|
284
|
+
copy[head] = replaceAt(copy[head], rest, replacement);
|
|
285
|
+
return copy;
|
|
286
|
+
}
|
|
287
|
+
const record = value;
|
|
288
|
+
return { ...record, [head]: replaceAt(record[head], rest, replacement) };
|
|
289
|
+
}
|
|
290
|
+
function toBytesForm(value) {
|
|
291
|
+
if (isKoraBytesValue(value)) {
|
|
292
|
+
try {
|
|
293
|
+
return { value: base64ToBytes(value.$koraBytes), changed: true };
|
|
294
|
+
} catch {
|
|
295
|
+
return { value, changed: false };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return mapMembers(value, toBytesForm);
|
|
299
|
+
}
|
|
300
|
+
function toKoraBytesForm(value) {
|
|
301
|
+
if (value instanceof Uint8Array)
|
|
302
|
+
return { value: { $koraBytes: bytesToBase64(value) }, changed: true };
|
|
303
|
+
return mapMembers(value, toKoraBytesForm);
|
|
304
|
+
}
|
|
305
|
+
function mapMembers(value, convert) {
|
|
306
|
+
if (Array.isArray(value)) {
|
|
307
|
+
let changed = false;
|
|
308
|
+
const out = value.map((member) => {
|
|
309
|
+
const result = convert(member);
|
|
310
|
+
changed ||= result.changed;
|
|
311
|
+
return result.value;
|
|
312
|
+
});
|
|
313
|
+
return changed ? { value: out, changed } : { value, changed: false };
|
|
314
|
+
}
|
|
315
|
+
if (value !== null && typeof value === "object" && !(value instanceof Uint8Array)) {
|
|
316
|
+
let changed = false;
|
|
317
|
+
const out = {};
|
|
318
|
+
for (const [key, member] of Object.entries(value)) {
|
|
319
|
+
const result = convert(member);
|
|
320
|
+
changed ||= result.changed;
|
|
321
|
+
out[key] = result.value;
|
|
322
|
+
}
|
|
323
|
+
return changed ? { value: out, changed } : { value, changed: false };
|
|
324
|
+
}
|
|
325
|
+
return { value, changed: false };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export {
|
|
329
|
+
buildScopeSnapshot,
|
|
330
|
+
matchesScopePredicate,
|
|
331
|
+
recordMatchesScopePredicates,
|
|
332
|
+
SYNC_PROTOCOL_VERSION,
|
|
333
|
+
LEGACY_SYNC_PROTOCOL_VERSION,
|
|
334
|
+
PROTOCOL_V1_DEPRECATED,
|
|
335
|
+
INVALID_OPERATION_ID,
|
|
336
|
+
PLAINTEXT_REJECTED,
|
|
337
|
+
declaredProtocolVersion,
|
|
338
|
+
scopeViewKey,
|
|
339
|
+
MemoryQueueStorage,
|
|
340
|
+
verifyInboundOperation,
|
|
341
|
+
operationIdMatches,
|
|
342
|
+
restoreUndefinedFromPrevious
|
|
343
|
+
};
|
|
344
|
+
//# sourceMappingURL=chunk-5S6U7EVU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scopes/scope-snapshot.ts","../src/protocol/protocol-version.ts","../src/scopes/scope-view-key.ts","../src/engine/memory-queue-storage.ts","../src/engine/verify-inbound.ts"],"sourcesContent":["import type { Operation } from '@korajs/core'\n\n/** Options for {@link buildScopeSnapshot}. */\nexport interface ScopeSnapshotOptions {\n\t/**\n\t * Layer the writer's `previousData` between the stored record and `op.data`.\n\t * Off by default: `previousData` is supplied by the writer and is never checked\n\t * against the stored row, so a server visibility decision built on it lets one\n\t * tenant steer its operations into another tenant's log (RT-3). Opt in only\n\t * where the writer is trusted for the decision, such as a client filtering its\n\t * own local view.\n\t * @default false\n\t */\n\tincludePreviousData?: boolean\n}\n\n/**\n * Build the record snapshot a scope or query-subset predicate is evaluated against\n * for an operation.\n *\n * Layering is `fullRecord < data` (or `fullRecord < previousData < data` with\n * `includePreviousData`), so the snapshot approximates the record after the\n * operation. The record identity is NOT taken from any of those\n * layers: `id` is always `op.recordId`, assigned last. An operation (or a stale\n * stored row) can therefore never claim to be a different record than the one it\n * actually targets, which is what made `previousData: { id: '<allowed>' }` a write\n * bypass under id-scoped predicates.\n *\n * This is the single snapshot builder shared by the client scope filter, the client\n * query-subset filter and the server scope filter. It is used for visibility\n * decisions only; upload authorization never reads `previousData` (see the server's\n * `authorizeUplinkWrite`).\n *\n * @param op - The operation being judged\n * @param fullRecord - Optional stored record state used to fill fields the op omits\n * @param options - Whether to trust the writer's `previousData` (default: no)\n * @returns The snapshot, always carrying `id === op.recordId`\n */\nexport function buildScopeSnapshot(\n\top: Operation,\n\tfullRecord?: Record<string, unknown> | null,\n\toptions: ScopeSnapshotOptions = {},\n): Record<string, unknown> {\n\treturn {\n\t\t...(fullRecord ?? {}),\n\t\t...(options.includePreviousData ? (asPlainRecord(op.previousData) ?? {}) : {}),\n\t\t...(asPlainRecord(op.data) ?? {}),\n\t\tid: op.recordId,\n\t}\n}\n\n/**\n * True when `actual` satisfies a scope predicate value: either an exact value\n * (compared with `Object.is`) or a bounded `{ $in: [...] }` set. An `undefined` or\n * `null` predicate value (or `$in` member) never matches.\n *\n * @param actual - The record's field value\n * @param expected - The scope predicate for that field\n * @returns Whether the value is inside the predicate\n */\nexport function matchesScopePredicate(actual: unknown, expected: unknown): boolean {\n\t// Fail closed (RT-8): an undefined/null predicate value would otherwise match\n\t// every record that lacks the field. The server refuses such grants outright;\n\t// this keeps any other caller from widening by accident.\n\tif (expected === undefined || expected === null) return false\n\tif (typeof expected === 'object' && !Array.isArray(expected) && '$in' in expected) {\n\t\tconst values = (expected as { $in?: unknown }).$in\n\t\treturn (\n\t\t\tArray.isArray(values) &&\n\t\t\tvalues.some((value) => value !== undefined && value !== null && Object.is(actual, value))\n\t\t)\n\t}\n\treturn Object.is(actual, expected)\n}\n\n/**\n * True when every field predicate in a collection scope matches the record.\n *\n * @param record - The record (or snapshot) to test\n * @param collectionScope - Field predicates for the record's collection\n * @returns Whether the record is inside the scope\n */\nexport function recordMatchesScopePredicates(\n\trecord: Record<string, unknown>,\n\tcollectionScope: Record<string, unknown>,\n): boolean {\n\tfor (const [field, expected] of Object.entries(collectionScope)) {\n\t\tif (!matchesScopePredicate(record[field], expected)) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunction asPlainRecord(value: unknown): Record<string, unknown> | null {\n\tif (typeof value !== 'object' || value === null || Array.isArray(value)) {\n\t\treturn null\n\t}\n\treturn value as Record<string, unknown>\n}\n","/**\n * Sync protocol version this build speaks (decision D2: one bump carrying all wire\n * breaks). Version 2 adds:\n * - operation ids hashed with content-hash version 2 (CORE-1), with `hashVersion`\n * carried on the wire and verified on receive;\n * - the end-to-end encryption envelope v2 (`op.encrypted`, AES-GCM AAD, ENC-3 and\n * NEW-ENC-1);\n * - the sequence-reservation capability on every handshake (RT-37);\n * - `authoritativeNodeIds` and server-authored `foldState` (W7).\n *\n * A handshake without `protocolVersion` is version 1 (Kora <= beta.12). Servers accept\n * version-1 clients for one release (beta.13) with a deprecation warning.\n */\nexport const SYNC_PROTOCOL_VERSION = 2\n\n/** Protocol version assumed when a handshake or response does not declare one. */\nexport const LEGACY_SYNC_PROTOCOL_VERSION = 1\n\n/** Code of the server's deprecation warning for a version-1 client. */\nexport const PROTOCOL_V1_DEPRECATED = 'PROTOCOL_V1_DEPRECATED'\n\n/** Non-retriable rejection code for an operation whose id is not its content hash. */\nexport const INVALID_OPERATION_ID = 'INVALID_OPERATION_ID'\n\n/**\n * Non-retriable rejection code for a plaintext operation uploaded to a server that\n * requires the encryption envelope.\n */\nexport const PLAINTEXT_REJECTED = 'PLAINTEXT_REJECTED'\n\n/**\n * The protocol version a handshake (or handshake response) declares.\n *\n * @param declared - The message's `protocolVersion`\n * @returns The declared version, or 1 when absent or malformed\n */\nexport function declaredProtocolVersion(declared: unknown): number {\n\treturn typeof declared === 'number' && Number.isInteger(declared) && declared >= 1\n\t\t? declared\n\t\t: LEGACY_SYNC_PROTOCOL_VERSION\n}\n","/**\n * Canonical key of a downlink scope map (SYNC-11). The client keys its per-view\n * delivery watermark by the scope the server ACCEPTED and reports that scope's key at\n * the next handshake; the server computes the key of the scope it resolves now and\n * resumes the delivery stream from the reported watermark only when the two match.\n * Both sides must therefore compute it the same way, from scopes that may have been\n * re-ordered or round-tripped through JSON: object keys are sorted, `undefined` entries\n * dropped, and `$in` lists de-duplicated and sorted. No scope (undefined or null) is\n * the empty string.\n */\nexport function scopeViewKey(\n\tscope: Record<string, Record<string, unknown>> | null | undefined,\n): string {\n\tif (scope === null || scope === undefined) return ''\n\treturn canonicalJson(normalizeValue(scope))\n}\n\nfunction normalizeValue(value: unknown): unknown {\n\tif (Array.isArray(value)) return value.map(normalizeValue)\n\tif (value === null || typeof value !== 'object') return value\n\tconst record = value as Record<string, unknown>\n\tconst out: Record<string, unknown> = {}\n\tfor (const key of Object.keys(record)) {\n\t\tconst entry = record[key]\n\t\tif (entry === undefined) continue\n\t\tif (key === '$in' && Array.isArray(entry)) {\n\t\t\tconst byKey = new Map<string, unknown>()\n\t\t\tfor (const item of entry) {\n\t\t\t\tconst normalized = normalizeValue(item)\n\t\t\t\tbyKey.set(canonicalJson(normalized), normalized)\n\t\t\t}\n\t\t\tout[key] = [...byKey.keys()].sort().map((k) => byKey.get(k))\n\t\t\tcontinue\n\t\t}\n\t\tout[key] = normalizeValue(entry)\n\t}\n\treturn out\n}\n\nfunction canonicalJson(value: unknown): string {\n\tif (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'\n\tif (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`\n\tconst record = value as Record<string, unknown>\n\tconst entries = Object.keys(record)\n\t\t.sort()\n\t\t.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)\n\treturn `{${entries.join(',')}}`\n}\n","import type { Operation } from '@korajs/core'\nimport type { QueueStorage } from '../types'\n\n/**\n * In-memory QueueStorage implementation for testing.\n * Operations are lost on process restart — production should use\n * IndexedDB or OPFS-backed storage.\n */\nexport class MemoryQueueStorage implements QueueStorage {\n\tprivate operations: Map<string, Operation> = new Map()\n\tprivate readonly sent = new Set<string>()\n\n\tasync load(): Promise<Operation[]> {\n\t\treturn [...this.operations.values()]\n\t}\n\n\tasync enqueue(op: Operation): Promise<void> {\n\t\tthis.operations.set(op.id, op)\n\t}\n\n\tasync dequeue(ids: string[]): Promise<void> {\n\t\tfor (const id of ids) {\n\t\t\tthis.operations.delete(id)\n\t\t\tthis.sent.delete(id)\n\t\t}\n\t}\n\n\tasync markSent(ops: Operation[]): Promise<void> {\n\t\tfor (const op of ops) {\n\t\t\tif (this.operations.has(op.id)) this.sent.add(op.id)\n\t\t}\n\t}\n\n\tasync loadSentIds(): Promise<string[]> {\n\t\treturn [...this.sent]\n\t}\n\n\tasync count(): Promise<number> {\n\t\treturn this.operations.size\n\t}\n}\n","import {\n\tbase64ToBytes,\n\tbytesToBase64,\n\tcanonicalizeLegacyOperation,\n\tisKoraBytesValue,\n\tverifyOperationId,\n} from '@korajs/core'\nimport type { FieldDescriptor, Operation, SchemaDefinition } from '@korajs/core'\nimport { INVALID_OPERATION_ID } from '../protocol/protocol-version'\n\n/** Prefix of node ids reserved for Kora itself (server-authored and server-synthesized). */\nconst RESERVED_NODE_PREFIX = 'kora:'\n\n/** Node of server-synthesized scope-entry operations (ids derived from the trigger op). */\nconst SCOPE_ENTRY_NODE_ID = 'kora:scope-entry'\n\n/**\n * Whether an operation's id is exempt from content verification: it is not a content\n * hash by construction. Only two kinds of operation qualify, both on Kora's reserved\n * nodes, which no device can claim:\n * - scope entries (`kora:scope-entry`): the id names the triggering operation, and the\n * data is the record's current row;\n * - server-derived operations (cascades, set-nulls, constraint corrections) of a\n * `kora:server:` node: the id is a keyed HMAC (RT-64) and they declare no hash version.\n *\n * A reserved-node operation that declares a hash version (a server route write, built\n * with `createOperation`) claims a content-addressed id and is verified like any other.\n */\nfunction isExemptFromIdVerification(op: Operation): boolean {\n\tif (!op.nodeId.startsWith(RESERVED_NODE_PREFIX)) return false\n\treturn op.nodeId === SCOPE_ENTRY_NODE_ID || op.hashVersion === undefined\n}\n\n/** Outcome of {@link verifyInboundOperation}. */\nexport type InboundVerification =\n\t| {\n\t\t\tok: true\n\t\t\t/** Whether the id was checked (false: not content-addressed, or not declared). */\n\t\t\tverified: boolean\n\t\t\t/**\n\t\t\t * The hash version the id matched, when it declared none (`'verify-ids'`): 1, or 2\n\t\t\t * for a version-2 id whose declaration was lost on the way.\n\t\t\t */\n\t\t\tmatchedVersion?: 1 | 2\n\t\t\t/**\n\t\t\t * False when the id matched only through the schema-dependent rebuild of a\n\t\t\t * beta.12 hash (declared nested object members that were `undefined`): a\n\t\t\t * receiver without the schema cannot repeat that check, so the server must not\n\t\t\t * declare the matched version on the stored copy. Absent: true.\n\t\t\t */\n\t\t\tdeclarable?: boolean\n\t\t\t/**\n\t\t\t * The update's data with each top-level member beta.12 hashed as `undefined`\n\t\t\t * restored as `null` (RT-71): beta.12 cleared those fields locally, and `null`\n\t\t\t * hashes identically under version 1, so the stored copy carries the write the\n\t\t\t * beta.12 client made, under the same verified id. Present only when the id\n\t\t\t * matched through that rebuild.\n\t\t\t */\n\t\t\trestoredData?: Operation['data']\n\t }\n\t| { ok: false; code: typeof INVALID_OPERATION_ID; message: string }\n\n/** How {@link verifyInboundOperation} treats an operation that declares no hash version. */\nexport type AbsentHashVersionPolicy =\n\t/**\n\t * Not checked (clients). The server declares the version of every id it verified\n\t * (1 or 2) when it stores the operation; an absent version marks an id the server\n\t * could not verify: a log row from before beta.13, a server-side schema transform\n\t * (which rewrites data under the original id), or a protocol-1 encrypted payload.\n\t */\n\t| 'skip'\n\t/**\n\t * Checked (the server, on upload: omitting the version skips nothing): the id must be\n\t * the operation's version-1 or version-2 content hash.\n\t */\n\t| 'verify-ids'\n\n/**\n * Verify a delivered (or uploaded) operation's content-addressed id (CORE-1, RT-64).\n * Runs AFTER decryption, on the plaintext the id was computed over, and before any\n * transform or apply. A mismatch means the operation was altered after it was created\n * (in transit, by a relay, or a ciphertext swap that kept its id), or that its id was\n * chosen rather than computed (squatting an id the server derives).\n *\n * Rules:\n * - Scope entries and server-derived operations (reserved `kora:` nodes, no declared hash\n * version: keyed HMAC ids) are not checked. A reserved-node operation that declares a\n * hash version (a server route write) is checked like any other.\n * - An operation that arrived in an encryption envelope is always checked, against\n * the hash version it declares (bound into the envelope's authenticated data).\n * - A plaintext operation declaring hash version 2 or 1 is checked against that version.\n * Version 1 covers type, collection, recordId, data, timestamp, nodeId and atomicOps;\n * binary values are accepted in either of their two forms (bytes, or the canonical\n * `{ $koraBytes }` form a JSON round trip produces), since a version-1 hash was\n * computed over whichever form the writer held.\n * - A plaintext operation declaring no version follows `absentVersion`: skipped, or\n * accepted when its id is its version-1 or version-2 content hash.\n * - An unknown declared version fails closed.\n *\n * @param op - The operation, decrypted\n * @param context - Whether it arrived in an encryption envelope, and the policy for an\n * operation that declares no hash version (default `'skip'`)\n * @returns ok, or the refusal with a code and an explanation\n */\nexport async function verifyInboundOperation(\n\top: Operation,\n\tcontext: {\n\t\tencrypted: boolean\n\t\tabsentVersion?: AbsentHashVersionPolicy\n\t\t/** The schema, for rebuilding beta.12 hashes of `undefined` nested members (RT-71). */\n\t\tschema?: SchemaDefinition | null\n\t},\n): Promise<InboundVerification> {\n\tif (isExemptFromIdVerification(op)) return { ok: true, verified: false }\n\tconst declared = op.hashVersion\n\tif (declared !== undefined && declared !== 1 && declared !== 2) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tcode: INVALID_OPERATION_ID,\n\t\t\tmessage: `Operation \"${op.id}\" declares unknown content-hash version ${String(declared)}; it cannot be verified and is not applied. Upgrade this client.`,\n\t\t}\n\t}\n\tif (\n\t\t!context.encrypted &&\n\t\tdeclared === undefined &&\n\t\t(context.absentVersion ?? 'skip') === 'skip'\n\t) {\n\t\treturn { ok: true, verified: false }\n\t}\n\tif (declared === undefined && !context.encrypted) {\n\t\tif (await verifyOperationId(op)) return { ok: true, verified: true, matchedVersion: 1 }\n\t\tif (await verifyOperationId({ ...op, hashVersion: 2 })) {\n\t\t\treturn { ok: true, verified: true, matchedVersion: 2 }\n\t\t}\n\t\tconst match = await matchOperationId(op, context.schema ?? null)\n\t\tif (match !== null) {\n\t\t\treturn {\n\t\t\t\tok: true,\n\t\t\t\tverified: true,\n\t\t\t\tmatchedVersion: 1,\n\t\t\t\t// Not declarable: a receiver without the schema cannot repeat a nested rebuild,\n\t\t\t\t// and a Date-form match never bound the Date's value (RT-90), so the stored\n\t\t\t\t// copy must not claim a verified version.\n\t\t\t\tdeclarable: !match.usedSchema && match.usedDateForm !== true,\n\t\t\t\t...(match.usedPrevious ? { restoredData: restoreUndefinedFromPrevious(op) } : {}),\n\t\t\t}\n\t\t}\n\t} else if ((await matchOperationId(op, context.schema ?? null)) !== null) {\n\t\treturn { ok: true, verified: true }\n\t}\n\treturn {\n\t\tok: false,\n\t\tcode: INVALID_OPERATION_ID,\n\t\tmessage: `Operation \"${op.id}\" from node \"${op.nodeId}\" does not match its content hash (hash version ${String(declared ?? 1)}${context.encrypted ? ', after decryption' : ''}): it was altered after it was created, or its id was not computed from its content. It is refused, not applied.`,\n\t}\n}\n\n/**\n * Whether the operation's id is its content hash under its declared version (absent:\n * 1). A version-1 id is also accepted over the other form of its binary values, and\n * over the beta.12 form of `undefined` members (see {@link matchOperationId}).\n *\n * @param op - The operation (plaintext)\n * @param schema - Optional schema, to rebuild declared nested members\n * @returns true when the id is the content hash\n */\nexport async function operationIdMatches(\n\top: Operation,\n\tschema: SchemaDefinition | null = null,\n): Promise<boolean> {\n\treturn (await matchOperationId(op, schema)) !== null\n}\n\n/** How an id matched: which beta.12 rebuilds (if any) its content needed. */\nexport interface OperationIdMatch {\n\t/** Top-level members restored from an update's `previousData` keys. */\n\tusedPrevious: boolean\n\t/** Declared nested members restored (needs the schema). */\n\tusedSchema: boolean\n\t/**\n\t * Strings in `Date#toISOString()` form hashed as `{}` (RT-90): beta.12 hashed a Date as\n\t * `{}`, so such an id never covered the Date's value.\n\t */\n\tusedDateForm?: boolean\n}\n\n/**\n * Match an operation's id against its content hash (RT-64, RT-71).\n *\n * Version 2 is checked as is: its canonical form is the JSON form (an `undefined`\n * member is absent, RT-72), so what was hashed is what arrives.\n *\n * Version 1 (beta.12 and earlier) hashed the in-memory value with `canonicalize`,\n * which writes an `undefined` object member as `\"key\":null`; the op log and the wire\n * are JSON, so the member is gone on arrival. The forms beta.12 actually produced are\n * rebuilt where they are recoverable:\n * - an update: beta.12 writes `previousData[key]` for every key it validated, so every\n * `previousData` key absent from `data` was an `undefined` value in `data`\n * (`update(id, { assignee: undefined })`);\n * - an object field (needs the schema): declared nested members absent from the value\n * (`{ a: 1, b: undefined }`), recursively for declared nested objects;\n * - a `Date` inside a json value (RT-90): `canonicalize` walks `Object.keys`, so it\n * hashed as `{}`, while the JSON log and wire hold its `toISOString()` string. Strings\n * in exactly that form are tried as `{}` (see {@link dateFormCandidates}).\n * Each form is also tried with the other form of its binary values (bytes, or the\n * canonical `{ $koraBytes }` form a JSON round trip produces).\n *\n * @param op - The operation (plaintext)\n * @param schema - The schema, or null (no nested rebuild)\n * @returns how the id matched, or null when it is not the content hash\n */\nexport async function matchOperationId(\n\top: Operation,\n\tschema: SchemaDefinition | null,\n): Promise<OperationIdMatch | null> {\n\tif (await verifyOperationId(op)) return { usedPrevious: false, usedSchema: false }\n\tif ((op.hashVersion ?? 1) !== 1) return null\n\tconst candidates: Array<{ data: Operation['data']; kind: OperationIdMatch }> = [\n\t\t{ data: op.data, kind: { usedPrevious: false, usedSchema: false } },\n\t]\n\tconst filled = restoreUndefinedFromPrevious(op)\n\tif (filled !== op.data) {\n\t\tcandidates.push({ data: filled, kind: { usedPrevious: true, usedSchema: false } })\n\t}\n\tif (schema) {\n\t\tfor (const candidate of [...candidates]) {\n\t\t\tconst nested = fillDeclaredNestedMembers(candidate.data, op.collection, schema)\n\t\t\tif (nested !== null) {\n\t\t\t\tcandidates.push({ data: nested, kind: { ...candidate.kind, usedSchema: true } })\n\t\t\t}\n\t\t}\n\t}\n\t// beta.12 (and older) hashed a Date inside a json value as `{}` (RT-90).\n\tfor (const candidate of [...candidates]) {\n\t\tfor (const data of dateFormCandidates(candidate.data)) {\n\t\t\tcandidates.push({ data, kind: { ...candidate.kind, usedDateForm: true } })\n\t\t}\n\t}\n\tfor (const candidate of candidates) {\n\t\tif (candidate.data !== op.data && (await verifyOperationId({ ...op, data: candidate.data }))) {\n\t\t\treturn candidate.kind\n\t\t}\n\t\tfor (const convert of [toBytesForm, toKoraBytesForm]) {\n\t\t\tconst data = convert(candidate.data)\n\t\t\tconst atomicOps = op.atomicOps === undefined ? undefined : convert(op.atomicOps)\n\t\t\tif (data.changed || atomicOps?.changed) {\n\t\t\t\tconst variant: Operation = {\n\t\t\t\t\t...op,\n\t\t\t\t\tdata: data.value as Operation['data'],\n\t\t\t\t\t...(atomicOps ? { atomicOps: atomicOps.value as Operation['atomicOps'] } : {}),\n\t\t\t\t}\n\t\t\t\tif (await verifyOperationId(variant)) return candidate.kind\n\t\t\t}\n\t\t}\n\t}\n\treturn null\n}\n\n/**\n * A beta.12 update's data with every `previousData` key it lacks restored as `null`\n * (RT-71). beta.12 writes `previousData[key]` for every key it validated, so such a key\n * held `undefined` in `data`: hashed as `null`, applied locally as a cleared field, and\n * dropped by the JSON upload. Returns `op.data` itself when there is none to restore.\n *\n * @param op - A plaintext operation\n * @returns The restored data, or `op.data` unchanged\n */\nexport function restoreUndefinedFromPrevious(op: Operation): Operation['data'] {\n\t// One rule for every replica: core's legacy canonical body (RT-71, RT-83).\n\treturn canonicalizeLegacyOperation({ ...op, hashVersion: undefined }).data\n}\n\n/**\n * The data with each declared nested member an object value lacks restored as `null`\n * (recursively), or null when nothing was restored.\n */\nfunction fillDeclaredNestedMembers(\n\tdata: Operation['data'],\n\tcollection: string,\n\tschema: SchemaDefinition,\n): Operation['data'] {\n\tconst fields = schema.collections[collection]?.fields\n\tif (data === null || fields === undefined) return null\n\tlet changed = false\n\tconst out: Record<string, unknown> = { ...data }\n\tfor (const [name, value] of Object.entries(data)) {\n\t\tconst descriptor = fields[name]\n\t\tif (descriptor === undefined) continue\n\t\tconst filled = fillNested(value, descriptor)\n\t\tif (filled !== value) {\n\t\t\tout[name] = filled\n\t\t\tchanged = true\n\t\t}\n\t}\n\treturn changed ? out : null\n}\n\nfunction fillNested(value: unknown, descriptor: FieldDescriptor): unknown {\n\tconst nested = descriptor.nestedFields\n\tif (descriptor.kind !== 'object' || !nested) return value\n\tif (value === null || typeof value !== 'object' || Array.isArray(value)) return value\n\tconst record = value as Record<string, unknown>\n\tlet changed = false\n\tconst out: Record<string, unknown> = { ...record }\n\tfor (const [name, child] of Object.entries(nested)) {\n\t\tif (!(name in record)) {\n\t\t\tout[name] = null\n\t\t\tchanged = true\n\t\t\tcontinue\n\t\t}\n\t\tconst filled = fillNested(record[name], child)\n\t\tif (filled !== record[name]) {\n\t\t\tout[name] = filled\n\t\t\tchanged = true\n\t\t}\n\t}\n\treturn changed ? out : value\n}\n\n/** `Date#toISOString()` output, including its extended-year form. */\nconst ISO_DATE_STRING = /^(?:\\d{4}|[+-]\\d{6})-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$/\n\n/**\n * Most ISO strings per operation whose every Date/string combination is tried; above\n * it only \"all of them were Dates\" is tried (bounds the hashing work to 2^6 - 1).\n */\nconst MAX_DATE_FORM_STRINGS = 6\n\n/**\n * The data with strings in `Date#toISOString()` form replaced by `{}`: how beta.12 (and\n * older) hashed a `Date` held in a json value (RT-90). Which of the strings were Dates\n * is unknown, so every non-empty subset is returned (all of them only, above\n * {@link MAX_DATE_FORM_STRINGS}). Empty when the data holds no such string.\n *\n * The stored data keeps the string, which is what beta.12 wrote to its row and applied.\n * Such an id never covered the Date's value (beta.12's hash did not either), so a match\n * through this rebuild is not declarable: the server stores the operation without a\n * declared hash version, as it stored every unverifiable protocol-1 id.\n */\nfunction dateFormCandidates(data: Operation['data']): Array<Operation['data']> {\n\tif (data === null) return []\n\tconst paths: Array<Array<string | number>> = []\n\tconst collect = (value: unknown, path: Array<string | number>): void => {\n\t\tif (typeof value === 'string') {\n\t\t\tif (ISO_DATE_STRING.test(value)) paths.push(path)\n\t\t\treturn\n\t\t}\n\t\tif (Array.isArray(value)) {\n\t\t\tvalue.forEach((item, index) => collect(item, [...path, index]))\n\t\t\treturn\n\t\t}\n\t\tif (value !== null && typeof value === 'object' && !(value instanceof Uint8Array)) {\n\t\t\tfor (const [key, item] of Object.entries(value)) collect(item, [...path, key])\n\t\t}\n\t}\n\tfor (const [field, value] of Object.entries(data)) collect(value, [field])\n\tif (paths.length === 0) return []\n\tconst subsets: Array<Array<Array<string | number>>> = []\n\tif (paths.length > MAX_DATE_FORM_STRINGS) {\n\t\tsubsets.push(paths)\n\t} else {\n\t\tfor (let mask = 1; mask < 1 << paths.length; mask++) {\n\t\t\tsubsets.push(paths.filter((_path, index) => (mask & (1 << index)) !== 0))\n\t\t}\n\t}\n\treturn subsets.map((subset) => {\n\t\tlet out: unknown = data\n\t\tfor (const path of subset) out = replaceAt(out, path, {})\n\t\treturn out as Operation['data']\n\t})\n}\n\n/** A copy of `value` with the member at `path` replaced (copy on write). */\nfunction replaceAt(\n\tvalue: unknown,\n\tpath: ReadonlyArray<string | number>,\n\treplacement: unknown,\n): unknown {\n\tconst [head, ...rest] = path\n\tif (head === undefined) return replacement\n\tif (Array.isArray(value)) {\n\t\tconst copy = [...value]\n\t\tcopy[head as number] = replaceAt(copy[head as number], rest, replacement)\n\t\treturn copy\n\t}\n\tconst record = value as Record<string, unknown>\n\treturn { ...record, [head]: replaceAt(record[head as string], rest, replacement) }\n}\n\ninterface Converted {\n\tvalue: unknown\n\tchanged: boolean\n}\n\n/** `{ $koraBytes }` values replaced by the bytes they encode. */\nfunction toBytesForm(value: unknown): Converted {\n\tif (isKoraBytesValue(value)) {\n\t\ttry {\n\t\t\treturn { value: base64ToBytes(value.$koraBytes), changed: true }\n\t\t} catch {\n\t\t\treturn { value, changed: false }\n\t\t}\n\t}\n\treturn mapMembers(value, toBytesForm)\n}\n\n/** Byte values replaced by their canonical `{ $koraBytes }` form. */\nfunction toKoraBytesForm(value: unknown): Converted {\n\tif (value instanceof Uint8Array)\n\t\treturn { value: { $koraBytes: bytesToBase64(value) }, changed: true }\n\treturn mapMembers(value, toKoraBytesForm)\n}\n\nfunction mapMembers(value: unknown, convert: (member: unknown) => Converted): Converted {\n\tif (Array.isArray(value)) {\n\t\tlet changed = false\n\t\tconst out = value.map((member) => {\n\t\t\tconst result = convert(member)\n\t\t\tchanged ||= result.changed\n\t\t\treturn result.value\n\t\t})\n\t\treturn changed ? { value: out, changed } : { value, changed: false }\n\t}\n\tif (value !== null && typeof value === 'object' && !(value instanceof Uint8Array)) {\n\t\tlet changed = false\n\t\tconst out: Record<string, unknown> = {}\n\t\tfor (const [key, member] of Object.entries(value)) {\n\t\t\tconst result = convert(member)\n\t\t\tchanged ||= result.changed\n\t\t\tout[key] = result.value\n\t\t}\n\t\treturn changed ? { value: out, changed } : { value, changed: false }\n\t}\n\treturn { value, changed: false }\n}\n"],"mappings":"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","names":[]}
|