@openlfcp/shared-objects 0.2.0-beta.2 → 0.2.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admission/framing.d.ts +11 -8
- package/dist/admission/framing.js +22 -12
- package/dist/sections/replica.js +1 -1
- package/dist/sections/tree.js +31 -4
- package/package.json +3 -3
|
@@ -29,15 +29,18 @@ export interface CheckedChange {
|
|
|
29
29
|
*/
|
|
30
30
|
export declare function checkChange(bytes: Uint8Array): CheckedChange;
|
|
31
31
|
/**
|
|
32
|
-
* The change hash of bytes checkChange refused, read without decoding
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
32
|
+
* The change hash of bytes checkChange refused, read without decoding them:
|
|
33
|
+
* the SHA-256 of the chunk from its type byte on (the Automerge change hash).
|
|
34
|
+
* §14.1 names a refused change by this hash whenever the bytes are one change
|
|
35
|
+
* chunk of type 1 whose length field covers them exactly and whose checksum
|
|
36
|
+
* matches — the inner header is never read, so a change above a §11.1 limit
|
|
37
|
+
* (named without being expanded) and one whose header holds a length running
|
|
38
|
+
* past its end (finding C3) are both named. Undefined when the bytes are not
|
|
39
|
+
* such a chunk: no magic bytes, another chunk type (a compressed chunk
|
|
40
|
+
* included), a length that does not cover them exactly, or a wrong checksum
|
|
41
|
+
* (whose name would be its intact copy's, finding C1).
|
|
39
42
|
*/
|
|
40
|
-
export declare function refusedChangeHash(bytes: Uint8Array
|
|
43
|
+
export declare function refusedChangeHash(bytes: Uint8Array): string | undefined;
|
|
41
44
|
/** The change as its canonical bytes give it (§11.3), reusing the walk of checkChange. */
|
|
42
45
|
export declare function parsedOf(change: CheckedChange): ParsedChange;
|
|
43
46
|
/** The decoded change, reusing the decode of checkChange when there was one. */
|
|
@@ -3,7 +3,7 @@ import { LfcpError, toHex } from "@openlfcp/core";
|
|
|
3
3
|
import { sha256 } from "@openlfcp/crypto";
|
|
4
4
|
import { ProfileInvalidError } from "../profile-invalid.js";
|
|
5
5
|
import { checkCanonicalChange } from "./canonical.js";
|
|
6
|
-
import { checkChangeExpansion
|
|
6
|
+
import { checkChangeExpansion } from "./limits.js";
|
|
7
7
|
/**
|
|
8
8
|
* Automerge byte checks of the SHARED-OBJECTS-PROFILE-01 payloads (LFCP-031):
|
|
9
9
|
* a Data Unit plaintext carries exactly one Automerge change (§11, §12), a
|
|
@@ -70,17 +70,18 @@ export function checkChange(bytes) {
|
|
|
70
70
|
return checked;
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
* The change hash of bytes checkChange refused, read without decoding
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
73
|
+
* The change hash of bytes checkChange refused, read without decoding them:
|
|
74
|
+
* the SHA-256 of the chunk from its type byte on (the Automerge change hash).
|
|
75
|
+
* §14.1 names a refused change by this hash whenever the bytes are one change
|
|
76
|
+
* chunk of type 1 whose length field covers them exactly and whose checksum
|
|
77
|
+
* matches — the inner header is never read, so a change above a §11.1 limit
|
|
78
|
+
* (named without being expanded) and one whose header holds a length running
|
|
79
|
+
* past its end (finding C3) are both named. Undefined when the bytes are not
|
|
80
|
+
* such a chunk: no magic bytes, another chunk type (a compressed chunk
|
|
81
|
+
* included), a length that does not cover them exactly, or a wrong checksum
|
|
82
|
+
* (whose name would be its intact copy's, finding C1).
|
|
80
83
|
*/
|
|
81
|
-
export function refusedChangeHash(bytes
|
|
82
|
-
if (error instanceof MalformedChunkError)
|
|
83
|
-
return undefined;
|
|
84
|
+
export function refusedChangeHash(bytes) {
|
|
84
85
|
if (bytes.length < HEADER || MAGIC.some((b, i) => bytes[i] !== b))
|
|
85
86
|
return undefined;
|
|
86
87
|
if (bytes[8] !== CHUNK_CHANGE)
|
|
@@ -100,7 +101,16 @@ export function refusedChangeHash(bytes, error) {
|
|
|
100
101
|
}
|
|
101
102
|
if (pos + length !== bytes.length)
|
|
102
103
|
return undefined;
|
|
103
|
-
|
|
104
|
+
// §14.1 (baseline.7, finding C1): name the bytes only when their checksum
|
|
105
|
+
// matches. The name is the SHA-256 of the chunk from its type byte on, and
|
|
106
|
+
// the checksum (bytes 4 to 8) is the first four bytes of that same hash; a
|
|
107
|
+
// chunk with a wrong checksum would be named by its intact copy's hash, so
|
|
108
|
+
// it is left unnamed and the intact copy is checked as any change is.
|
|
109
|
+
const hash = sha256(bytes.subarray(8));
|
|
110
|
+
for (let i = 0; i < 4; i++)
|
|
111
|
+
if (bytes[4 + i] !== hash[i])
|
|
112
|
+
return undefined;
|
|
113
|
+
return toHex(hash);
|
|
104
114
|
}
|
|
105
115
|
/** The operations checkChange read from the bytes (§11.3), for the references of §11.4. */
|
|
106
116
|
const PARSED = new WeakMap();
|
package/dist/sections/replica.js
CHANGED
|
@@ -292,7 +292,7 @@ export class SectionReplica {
|
|
|
292
292
|
}
|
|
293
293
|
catch (e) {
|
|
294
294
|
// Never decoded: a change refused by the checks is named from its bytes only.
|
|
295
|
-
refused.push(refusal(index, refusedChangeHash(unit.bytes
|
|
295
|
+
refused.push(refusal(index, refusedChangeHash(unit.bytes), e, false));
|
|
296
296
|
return;
|
|
297
297
|
}
|
|
298
298
|
if (unit.signer !== undefined &&
|
package/dist/sections/tree.js
CHANGED
|
@@ -189,8 +189,13 @@ export function deriveTree(doc, validation = validateSection(doc)) {
|
|
|
189
189
|
* the node or of an ancestor, from the change history. A change deletes
|
|
190
190
|
* when the last lifecycle value it writes to an owner (a Task node's Task,
|
|
191
191
|
* another node itself) is "deleted"; content is a node's creation, an
|
|
192
|
-
* operation on its Text,
|
|
193
|
-
*
|
|
192
|
+
* operation on its Text, a Task's title or status, or a move of the node
|
|
193
|
+
* (a write to its `placement` register — node.move or node.resolve_placement,
|
|
194
|
+
* §8). Two changes are concurrent when neither is in the other's dependencies.
|
|
195
|
+
* Only a deletion the lifecycle register still holds counts: one a later write
|
|
196
|
+
* to the same register depends on is superseded (deleting the node again clears
|
|
197
|
+
* the attention a concurrent edit raised), while two concurrent deletions both
|
|
198
|
+
* count (baseline.9).
|
|
194
199
|
*/
|
|
195
200
|
function retainedEdits(doc, hidden, parentOf, sectionId) {
|
|
196
201
|
const root = doc;
|
|
@@ -201,8 +206,13 @@ function retainedEdits(doc, hidden, parentOf, sectionId) {
|
|
|
201
206
|
const textOf = new Map();
|
|
202
207
|
const ownerOf = new Map();
|
|
203
208
|
const taskOf = new Map();
|
|
209
|
+
// The object ID of each node's own map, for a move written to its `placement`.
|
|
210
|
+
const nodeMapOf = new Map();
|
|
204
211
|
for (const id of Object.keys(nodes)) {
|
|
205
212
|
const n = nodes[id];
|
|
213
|
+
const nodeMap = A.getObjectId(n);
|
|
214
|
+
if (nodeMap !== null)
|
|
215
|
+
nodeMapOf.set(nodeMap, id);
|
|
206
216
|
const text = A.getObjectId(n, "text");
|
|
207
217
|
if (text !== null)
|
|
208
218
|
textOf.set(text, id);
|
|
@@ -219,6 +229,8 @@ function retainedEdits(doc, hidden, parentOf, sectionId) {
|
|
|
219
229
|
}
|
|
220
230
|
const deps = new Map();
|
|
221
231
|
const deletes = [];
|
|
232
|
+
// Every lifecycle write (any value), to tell whether a deletion is still held.
|
|
233
|
+
const lifecycleWrites = [];
|
|
222
234
|
const edits = [];
|
|
223
235
|
for (const bytes of A.getAllChanges(doc)) {
|
|
224
236
|
const change = A.decodeChange(bytes);
|
|
@@ -236,13 +248,21 @@ function retainedEdits(doc, hidden, parentOf, sectionId) {
|
|
|
236
248
|
const task = taskOf.get(op.obj);
|
|
237
249
|
if (task !== undefined && hidden.has(task) && (key === "title" || key === "status"))
|
|
238
250
|
touched.add(task);
|
|
251
|
+
// §8, §9: a move writes the node's `placement` register (node.move or
|
|
252
|
+
// node.resolve_placement). A creation also writes it and is already
|
|
253
|
+
// counted above; what this adds is a move of an existing node.
|
|
254
|
+
const moved = nodeMapOf.get(op.obj);
|
|
255
|
+
if (moved !== undefined && key === "placement" && hidden.has(moved))
|
|
256
|
+
touched.add(moved);
|
|
239
257
|
const owner = ownerOf.get(op.obj);
|
|
240
258
|
if (owner !== undefined && key === "lifecycle")
|
|
241
259
|
lastLifecycle.set(owner, value);
|
|
242
260
|
}
|
|
243
|
-
for (const [owner, value] of lastLifecycle)
|
|
261
|
+
for (const [owner, value] of lastLifecycle) {
|
|
262
|
+
lifecycleWrites.push({ hash: change.hash, node: owner });
|
|
244
263
|
if (value === "deleted")
|
|
245
264
|
deletes.push({ hash: change.hash, node: owner });
|
|
265
|
+
}
|
|
246
266
|
for (const node of touched)
|
|
247
267
|
edits.push({ hash: change.hash, node });
|
|
248
268
|
}
|
|
@@ -269,8 +289,15 @@ function retainedEdits(doc, hidden, parentOf, sectionId) {
|
|
|
269
289
|
return out;
|
|
270
290
|
};
|
|
271
291
|
const concurrent = (a, b) => a !== b && !ancestorsOf(a).has(b) && !ancestorsOf(b).has(a);
|
|
292
|
+
// §9 (baseline.9): a deletion counts only while the lifecycle register still
|
|
293
|
+
// holds it. A later write to the same register — one that has the deletion in
|
|
294
|
+
// its dependencies — supersedes it, so deleting a node again clears the
|
|
295
|
+
// attention a concurrent edit raised. Two concurrent deletions supersede
|
|
296
|
+
// neither, so both still count (SS86); the writes come from each change's
|
|
297
|
+
// operations, which also keeps a deletion merged after a concurrent one.
|
|
298
|
+
const heldDeletes = deletes.filter((d) => !lifecycleWrites.some((w) => w.node === d.node && w.hash !== d.hash && ancestorsOf(w.hash).has(d.hash)));
|
|
272
299
|
const deletesOf = new Map();
|
|
273
|
-
for (const d of
|
|
300
|
+
for (const d of heldDeletes)
|
|
274
301
|
deletesOf.set(d.node, [...(deletesOf.get(d.node) ?? []), d.hash]);
|
|
275
302
|
const attention = new Set();
|
|
276
303
|
for (const edit of edits) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlfcp/shared-objects",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.4",
|
|
4
4
|
"description": "SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1) implementation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openlfcp",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@automerge/automerge": "3.5.0",
|
|
55
55
|
"fflate": "0.8.3",
|
|
56
|
-
"@openlfcp/core": "^0.2.0-beta.
|
|
57
|
-
"@openlfcp/crypto": "^0.2.0-beta.
|
|
56
|
+
"@openlfcp/core": "^0.2.0-beta.4",
|
|
57
|
+
"@openlfcp/crypto": "^0.2.0-beta.4"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"fast-check": "4.10.2"
|