functionalscript 0.45.0 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/fjs/asn.1/module.f.mjs +8 -1
- package/fjs/asn.1/proof.f.d.mts +10 -0
- package/fjs/asn.1/proof.f.mjs +16 -0
- package/fjs/basen/base128/module.f.mjs +22 -5
- package/fjs/bnf/data/module.f.d.mts +32 -6
- package/fjs/bnf/data/module.f.mjs +112 -7
- package/fjs/bnf/data/proof.f.d.mts +2 -0
- package/fjs/bnf/data/proof.f.mjs +74 -3
- package/fjs/bnf/data/types.d.ts +20 -2
- package/fjs/bnf/descent/module.f.d.mts +28 -14
- package/fjs/bnf/descent/module.f.mjs +122 -77
- package/fjs/bnf/descent/proof.f.d.mts +2 -0
- package/fjs/bnf/descent/proof.f.mjs +117 -31
- package/fjs/bnf/descent/types.d.ts +12 -14
- package/fjs/bnf/ll1/module.f.d.mts +64 -22
- package/fjs/bnf/ll1/module.f.mjs +214 -154
- package/fjs/bnf/ll1/proof.f.d.mts +15 -2
- package/fjs/bnf/ll1/proof.f.mjs +323 -149
- package/fjs/bnf/ll1/types.d.ts +16 -24
- package/fjs/bnf/matcher/module.f.d.mts +66 -0
- package/fjs/bnf/matcher/module.f.mjs +81 -0
- package/fjs/bnf/matcher/proof.f.d.mts +10 -0
- package/fjs/bnf/matcher/proof.f.mjs +79 -0
- package/fjs/bnf/matcher/types.d.ts +54 -0
- package/fjs/bnf/testlib.f.d.mts +31 -0
- package/fjs/bnf/testlib.f.mjs +80 -0
- package/fjs/cas/cli/module.f.d.mts +1 -1
- package/fjs/cas/cli/module.f.mjs +14 -20
- package/fjs/cas/cli/proof.f.d.mts +1 -3
- package/fjs/cas/cli/proof.f.mjs +44 -33
- package/fjs/cas/evo/module.f.d.mts +64 -18
- package/fjs/cas/evo/module.f.mjs +148 -70
- package/fjs/cas/evo/proof.f.d.mts +10 -1
- package/fjs/cas/evo/proof.f.mjs +305 -223
- package/fjs/cas/evo/types.d.ts +45 -9
- package/fjs/cas/module.f.d.mts +18 -32
- package/fjs/cas/module.f.mjs +129 -128
- package/fjs/cas/proof.f.d.mts +11 -8
- package/fjs/cas/proof.f.mjs +259 -147
- package/fjs/cas/types.d.ts +24 -10
- package/fjs/ci/config/module.f.d.mts +2 -2
- package/fjs/ci/config/module.f.mjs +2 -2
- package/fjs/ci/module.f.d.mts +5 -5
- package/fjs/ci/module.f.mjs +8 -7
- package/fjs/ci/nix/module.f.d.mts +7 -5
- package/fjs/ci/nix/module.f.mjs +13 -12
- package/fjs/ci/nix/proof.f.mjs +2 -2
- package/fjs/ci/proof.f.mjs +9 -5
- package/fjs/cli/module.f.d.mts +4 -6
- package/fjs/cli/module.f.mjs +4 -8
- package/fjs/cli/proof.f.mjs +17 -16
- package/fjs/cli/types.d.ts +2 -3
- package/fjs/common/monoid/types.d.ts +1 -1
- package/fjs/crypto/hmac/module.f.mjs +2 -2
- package/fjs/crypto/sha2/module.f.mjs +3 -1
- package/fjs/crypto/sha2/proof.f.d.mts +1 -0
- package/fjs/crypto/sha2/proof.f.mjs +24 -0
- package/fjs/crypto/sha2/types.d.ts +11 -0
- package/fjs/crypto/sign/module.f.mjs +2 -2
- package/fjs/dev/module.f.d.mts +13 -4
- package/fjs/dev/module.f.mjs +56 -27
- package/fjs/dev/update/module.f.d.mts +9 -4
- package/fjs/dev/update/module.f.mjs +14 -10
- package/fjs/dev/update/proof.f.d.mts +1 -3
- package/fjs/dev/update/proof.f.mjs +10 -5
- package/fjs/djs/module.f.d.mts +13 -5
- package/fjs/djs/module.f.mjs +31 -16
- package/fjs/djs/parser/module.f.d.mts +13 -3
- package/fjs/djs/parser/module.f.mjs +117 -16
- package/fjs/djs/parser/proof.f.d.mts +5 -1
- package/fjs/djs/parser/proof.f.mjs +274 -12
- package/fjs/djs/parser/types.d.ts +7 -1
- package/fjs/djs/proof.f.d.mts +18 -2
- package/fjs/djs/proof.f.mjs +187 -12
- package/fjs/djs/serializer/module.f.d.mts +25 -8
- package/fjs/djs/serializer/module.f.mjs +61 -15
- package/fjs/djs/serializer/proof.f.d.mts +5 -0
- package/fjs/djs/serializer/proof.f.mjs +23 -1
- package/fjs/djs/tokenizer/module.f.d.mts +17 -5
- package/fjs/djs/tokenizer/module.f.mjs +99 -60
- package/fjs/djs/tokenizer/proof.f.d.mts +1 -1
- package/fjs/djs/tokenizer/proof.f.mjs +74 -67
- package/fjs/djs/transpiler/module.f.d.mts +12 -7
- package/fjs/djs/transpiler/module.f.mjs +82 -56
- package/fjs/djs/transpiler/types.d.ts +7 -3
- package/fjs/djs/types.d.ts +7 -1
- package/fjs/effects/list/module.f.d.mts +14 -11
- package/fjs/effects/list/module.f.mjs +13 -11
- package/fjs/effects/list/types.d.ts +27 -7
- package/fjs/effects/memory/module.f.d.mts +2 -1
- package/fjs/effects/memory/module.f.mjs +3 -4
- package/fjs/effects/memory/proof.f.mjs +13 -7
- package/fjs/effects/memory/types.d.ts +4 -3
- package/fjs/effects/mock/module.f.d.mts +20 -4
- package/fjs/effects/mock/module.f.mjs +37 -5
- package/fjs/effects/mock/types.d.ts +10 -1
- package/fjs/effects/module.d.mts +4 -2
- package/fjs/effects/module.f.d.mts +466 -280
- package/fjs/effects/module.f.mjs +537 -299
- package/fjs/effects/module.mjs +2 -1
- package/fjs/effects/node/memory/module.d.mts +4 -2
- package/fjs/effects/node/memory/module.mjs +6 -3
- package/fjs/effects/node/memory/proof.mjs +10 -4
- package/fjs/effects/node/module.d.mts +4 -3
- package/fjs/effects/node/module.f.d.mts +168 -33
- package/fjs/effects/node/module.f.mjs +256 -52
- package/fjs/effects/node/module.mjs +57 -34
- package/fjs/effects/node/proof.f.d.mts +28 -2
- package/fjs/effects/node/proof.f.mjs +161 -42
- package/fjs/effects/node/types.d.ts +106 -18
- package/fjs/effects/node/virtual/module.f.d.mts +18 -4
- package/fjs/effects/node/virtual/module.f.mjs +110 -68
- package/fjs/effects/node/virtual/proof.f.d.mts +28 -2
- package/fjs/effects/node/virtual/proof.f.mjs +190 -9
- package/fjs/effects/proof.f.d.mts +69 -37
- package/fjs/effects/proof.f.mjs +410 -130
- package/fjs/effects/types.d.ts +161 -33
- package/fjs/emergent_testing/module.f.d.mts +19 -12
- package/fjs/emergent_testing/module.f.mjs +93 -33
- package/fjs/emergent_testing/proof.f.d.mts +21 -7
- package/fjs/emergent_testing/proof.f.mjs +166 -32
- package/fjs/emergent_testing/types.d.ts +22 -4
- package/fjs/fsm/module.f.d.mts +14 -4
- package/fjs/fsm/module.f.mjs +54 -37
- package/fjs/fsm/proof.f.d.mts +2 -0
- package/fjs/fsm/proof.f.mjs +83 -114
- package/fjs/js/keywords/module.f.d.mts +52 -0
- package/fjs/js/keywords/module.f.mjs +72 -0
- package/fjs/js/keywords/proof.f.d.mts +3 -0
- package/fjs/js/keywords/proof.f.mjs +13 -0
- package/fjs/js/tokenizer/module.f.d.mts +26 -6
- package/fjs/js/tokenizer/module.f.mjs +145 -159
- package/fjs/js/tokenizer/proof.f.d.mts +1 -0
- package/fjs/js/tokenizer/proof.f.mjs +54 -24
- package/fjs/js/tokenizer/types.d.ts +33 -24
- package/fjs/mcp/cas/module.f.d.mts +1 -6
- package/fjs/mcp/cas/module.f.mjs +55 -51
- package/fjs/mcp/cas/proof.f.d.mts +15 -0
- package/fjs/mcp/cas/proof.f.mjs +174 -0
- package/fjs/mcp/evo/module.f.d.mts +19 -10
- package/fjs/mcp/evo/module.f.mjs +48 -27
- package/fjs/mcp/evo/proof.f.d.mts +6 -1
- package/fjs/mcp/evo/proof.f.mjs +115 -31
- package/fjs/mcp/module.f.d.mts +4 -4
- package/fjs/mcp/module.f.mjs +6 -6
- package/fjs/mcp/proof.f.d.mts +5 -3
- package/fjs/mcp/proof.f.mjs +112 -46
- package/fjs/media/html/module.f.mjs +1 -1
- package/fjs/media/json/extended/module.f.d.mts +82 -0
- package/fjs/media/json/extended/module.f.mjs +153 -0
- package/fjs/media/json/extended/proof.f.d.mts +42 -0
- package/fjs/media/json/extended/proof.f.mjs +127 -0
- package/fjs/media/json/extended/types.d.ts +23 -0
- package/fjs/media/json/module.f.d.mts +8 -2
- package/fjs/media/json/module.f.mjs +43 -41
- package/fjs/media/json/number/module.f.d.mts +59 -0
- package/fjs/media/json/number/module.f.mjs +136 -0
- package/fjs/media/json/number/proof.f.d.mts +24 -0
- package/fjs/media/json/number/proof.f.mjs +86 -0
- package/fjs/media/json/number/types.d.ts +28 -0
- package/fjs/media/json/parser/module.f.d.mts +25 -13
- package/fjs/media/json/parser/module.f.mjs +114 -70
- package/fjs/media/json/parser/proof.f.d.mts +5 -0
- package/fjs/media/json/parser/proof.f.mjs +31 -1
- package/fjs/media/json/parser/types.d.ts +33 -14
- package/fjs/media/json/rtti/module.f.d.mts +1 -1
- package/fjs/media/json/rtti/module.f.mjs +1 -1
- package/fjs/media/json/rtti/proof.f.mjs +9 -9
- package/fjs/media/json/schema/module.f.mjs +3 -13
- package/fjs/media/json/schema/proof.f.d.mts +0 -1
- package/fjs/media/json/schema/proof.f.mjs +1 -2
- package/fjs/media/json/serializer/module.f.d.mts +32 -1
- package/fjs/media/json/serializer/module.f.mjs +64 -2
- package/fjs/media/json/tokenizer/module.f.mjs +7 -3
- package/fjs/media/json/tokenizer/proof.f.d.mts +6 -0
- package/fjs/media/json/tokenizer/proof.f.mjs +62 -21
- package/fjs/media/json/types.d.ts +36 -10
- package/fjs/media/lock/module.f.d.mts +100 -0
- package/fjs/media/lock/module.f.mjs +125 -0
- package/fjs/media/lock/proof.f.d.mts +33 -0
- package/fjs/media/lock/proof.f.mjs +196 -0
- package/fjs/media/lock/types.d.ts +15 -0
- package/fjs/media/module.f.d.mts +6 -5
- package/fjs/media/module.f.mjs +8 -7
- package/fjs/media/note/module.f.d.mts +121 -0
- package/fjs/media/note/module.f.mjs +131 -0
- package/fjs/media/note/proof.f.d.mts +29 -0
- package/fjs/media/note/proof.f.mjs +150 -0
- package/fjs/media/note/types.d.ts +10 -0
- package/fjs/media/proof.f.d.mts +4 -1
- package/fjs/media/proof.f.mjs +40 -21
- package/fjs/media/revision/module.f.d.mts +78 -7
- package/fjs/media/revision/module.f.mjs +119 -12
- package/fjs/media/revision/proof.f.d.mts +10 -0
- package/fjs/media/revision/proof.f.mjs +88 -0
- package/fjs/media/revision/types.d.ts +34 -5
- package/fjs/media/type/module.f.d.mts +33 -15
- package/fjs/media/type/module.f.mjs +35 -29
- package/fjs/media/type/proof.f.d.mts +2 -1
- package/fjs/media/type/proof.f.mjs +30 -10
- package/fjs/module.f.mjs +29 -8
- package/fjs/nanvm/proof.f.mjs +3 -3
- package/fjs/nanvm/rust/module.f.mjs +1 -1
- package/fjs/nanvm/update/module.f.d.mts +4 -4
- package/fjs/nanvm/update/module.f.mjs +8 -9
- package/fjs/nanvm/update/proof.f.mjs +4 -3
- package/fjs/proof.f.d.mts +3 -3
- package/fjs/proof.f.mjs +36 -10
- package/fjs/protocol/json_rpc/module.f.d.mts +2 -2
- package/fjs/protocol/json_rpc/module.f.mjs +3 -3
- package/fjs/protocol/json_rpc/proof.f.mjs +4 -4
- package/fjs/protocol/mcp/module.f.d.mts +10 -13
- package/fjs/protocol/mcp/module.f.mjs +74 -58
- package/fjs/protocol/mcp/proof.f.d.mts +13 -4
- package/fjs/protocol/mcp/proof.f.mjs +193 -61
- package/fjs/protocol/mcp/stdio/module.f.d.mts +15 -7
- package/fjs/protocol/mcp/stdio/module.f.mjs +38 -25
- package/fjs/protocol/mcp/stdio/proof.f.d.mts +4 -2
- package/fjs/protocol/mcp/stdio/proof.f.mjs +45 -12
- package/fjs/protocol/mcp/stdio/types.d.ts +1 -1
- package/fjs/protocol/mcp/types.d.ts +17 -6
- package/fjs/sul/id/module.f.d.mts +0 -1
- package/fjs/sul/id/module.f.mjs +2 -3
- package/fjs/sul/level/hash/module.f.mjs +2 -1
- package/fjs/sul/level/hash/proof.f.mjs +2 -2
- package/fjs/sul/module.f.mjs +18 -13
- package/fjs/text/code_point/module.f.d.mts +8 -0
- package/fjs/text/code_point/module.f.mjs +8 -1
- package/fjs/text/code_point/proof.f.d.mts +1 -0
- package/fjs/text/code_point/proof.f.mjs +11 -0
- package/fjs/text/sgr/module.f.d.mts +4 -6
- package/fjs/text/sgr/module.f.mjs +6 -7
- package/fjs/text/utf16/module.f.mjs +7 -2
- package/fjs/text/utf16/proof.f.mjs +3 -3
- package/fjs/text/utf8/module.f.mjs +4 -2
- package/fjs/types/array/module.f.mjs +14 -2
- package/fjs/types/bit_vec/module.f.d.mts +0 -2
- package/fjs/types/bit_vec/module.f.mjs +46 -32
- package/fjs/types/bit_vec/proof.f.mjs +2 -2
- package/fjs/types/btree/remove/module.f.mjs +1 -1
- package/fjs/types/btree/set/module.f.mjs +9 -11
- package/fjs/types/btree/set/proof.f.mjs +12 -0
- package/fjs/types/byte_set/module.f.d.mts +11 -4
- package/fjs/types/byte_set/module.f.mjs +14 -6
- package/fjs/types/byte_set/proof.f.mjs +7 -7
- package/fjs/types/function/compare/module.f.mjs +10 -3
- package/fjs/types/list/module.f.d.mts +1 -1
- package/fjs/types/list/module.f.mjs +1 -1
- package/fjs/types/nullable/module.f.d.mts +18 -4
- package/fjs/types/nullable/module.f.mjs +21 -4
- package/fjs/types/nullable/proof.f.d.mts +4 -0
- package/fjs/types/nullable/proof.f.mjs +15 -0
- package/fjs/types/object/module.f.d.mts +12 -2
- package/fjs/types/object/module.f.mjs +11 -1
- package/fjs/types/patricia_trie/module.f.mjs +26 -11
- package/fjs/types/result/module.f.d.mts +3 -3
- package/fjs/types/result/module.f.mjs +3 -3
- package/fjs/types/rtti/common/module.f.d.mts +25 -28
- package/fjs/types/rtti/common/module.f.mjs +36 -32
- package/fjs/types/rtti/common/proof.f.mjs +5 -5
- package/fjs/types/rtti/data/module.f.d.mts +14 -0
- package/fjs/types/rtti/data/module.f.mjs +25 -2
- package/fjs/types/rtti/data/proof.f.d.mts +1 -0
- package/fjs/types/rtti/data/proof.f.mjs +25 -2
- package/fjs/types/rtti/parse/module.f.d.mts +24 -14
- package/fjs/types/rtti/parse/module.f.mjs +37 -28
- package/fjs/types/rtti/parse/proof.f.d.mts +3 -2
- package/fjs/types/rtti/parse/proof.f.mjs +33 -14
- package/fjs/types/rtti/proof.f.mjs +3 -1
- package/fjs/types/rtti/ts/module.f.mjs +13 -17
- package/fjs/types/rtti/ts/types.d.ts +14 -1
- package/fjs/types/sorted_set/module.f.d.mts +18 -0
- package/fjs/types/sorted_set/module.f.mjs +22 -0
- package/fjs/types/sorted_set/proof.f.d.mts +1 -0
- package/fjs/types/sorted_set/proof.f.mjs +16 -1
- package/fjs/types/uint8array/module.f.d.mts +1 -1
- package/fjs/types/uint8array/module.f.mjs +1 -1
- package/fjs/website/module.f.d.mts +3 -3
- package/fjs/website/module.f.mjs +4 -7
- package/fjs/website/proof.f.mjs +2 -1
- package/package.json +2 -2
- package/fjs/dev/package_json/module.f.d.mts +0 -39
- package/fjs/dev/package_json/module.f.mjs +0 -40
- package/fjs/dev/package_json/proof.f.d.mts +0 -6
- package/fjs/dev/package_json/proof.f.mjs +0 -32
- package/fjs/effects/eff/module.f.d.mts +0 -20
- package/fjs/effects/eff/module.f.mjs +0 -70
- package/fjs/effects/eff/proof.f.d.mts +0 -15
- package/fjs/effects/eff/proof.f.mjs +0 -69
- package/fjs/effects/eff/types.d.ts +0 -71
- package/fjs/types/rtti/validate/module.f.d.mts +0 -41
- package/fjs/types/rtti/validate/module.f.mjs +0 -171
- package/fjs/types/rtti/validate/proof.f.d.mts +0 -125
- package/fjs/types/rtti/validate/proof.f.mjs +0 -437
- package/fjs/types/rtti/validate/types.d.ts +0 -6
package/fjs/cas/evo/proof.f.mjs
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @import { Cas } from '../types.ts'
|
|
3
|
+
* @import { EvoChannel } from './types.ts'
|
|
4
|
+
* @import { Result } from '../../types/result/types.ts'
|
|
5
|
+
* @import { Effect } from '../../effects/types.ts'
|
|
6
|
+
* @import { IoChannel, NodeOp } from '../../effects/node/types.ts'
|
|
7
|
+
* @import { State } from '../../effects/node/virtual/types.ts'
|
|
3
8
|
* @import { Vec } from '../../types/bit_vec/types.ts'
|
|
4
9
|
* @import { Ok } from '../../types/result/types.ts'
|
|
5
|
-
* @import { IoResult } from '../../effects/node/types.ts'
|
|
6
10
|
* @import { List } from '../../effects/list/types.ts'
|
|
7
11
|
* @import { RevisionData } from './types.ts'
|
|
8
12
|
*/
|
|
@@ -13,13 +17,15 @@ import {
|
|
|
13
17
|
assertStructurallySame,
|
|
14
18
|
} from '../../asserts/module.f.mjs'
|
|
15
19
|
import { pure } from '../../effects/module.f.mjs'
|
|
20
|
+
import { pureError } from '../../effects/module.f.mjs'
|
|
21
|
+
import { ioError } from '../../effects/node/module.f.mjs'
|
|
16
22
|
import { fileCas } from '../module.f.mjs'
|
|
17
23
|
import { sha256 } from '../../crypto/sha2/module.f.mjs'
|
|
18
24
|
import { emptyState, virtual } from '../../effects/node/virtual/module.f.mjs'
|
|
19
25
|
import { vec, vec8 } from '../../types/bit_vec/module.f.mjs'
|
|
20
26
|
import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.mjs'
|
|
21
27
|
import { unwrap } from '../../types/nullable/module.f.mjs'
|
|
22
|
-
import { ok, error } from '../../types/result/module.f.mjs'
|
|
28
|
+
import { ok, error, unwrap as unwrapResult } from '../../types/result/module.f.mjs'
|
|
23
29
|
import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.mjs'
|
|
24
30
|
import { tryUtf8 } from '../../text/module.f.mjs'
|
|
25
31
|
import { dialect as revisionDialect } from '../../media/revision/module.f.mjs'
|
|
@@ -34,18 +40,18 @@ const home = '.'
|
|
|
34
40
|
/** @type {Cas<never>} */
|
|
35
41
|
const writeFailingCas = {
|
|
36
42
|
read: () => elEmpty(),
|
|
37
|
-
write: () => pure(error('boom')),
|
|
38
|
-
list: () => pure([]),
|
|
43
|
+
write: () => pure(error(ioError({ message: 'boom' }))),
|
|
44
|
+
list: () => pure(ok([])),
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
// A `Cas<never>` whose `read`
|
|
42
|
-
// shard — what a permission error, a mid-stream I/O failure, or a blob
|
|
43
|
-
// large for `collectRead` to buffer looks like to a caller.
|
|
47
|
+
// A `Cas<never>` whose `read` fails the stream with something that is *not* a
|
|
48
|
+
// missing shard — what a permission error, a mid-stream I/O failure, or a blob
|
|
49
|
+
// too large for `collectRead` to buffer looks like to a caller.
|
|
44
50
|
/** @type {Cas<never>} */
|
|
45
51
|
const readFailingCas = {
|
|
46
|
-
read: () =>
|
|
47
|
-
write: () => pure(error('write not supported')),
|
|
48
|
-
list: () => pure([]),
|
|
52
|
+
read: () => pureError(ioError({ message: 'boom' })),
|
|
53
|
+
write: () => pure(error(ioError({ message: 'write not supported' }))),
|
|
54
|
+
list: () => pure(ok([])),
|
|
49
55
|
}
|
|
50
56
|
|
|
51
57
|
// A `Cas<never>` backed by a fixed set of (hash, content) entries, returned
|
|
@@ -57,45 +63,80 @@ const fixedCas = entries => ({
|
|
|
57
63
|
read: hash => {
|
|
58
64
|
const found = entries.find(([h]) => vecToCBase32(h) === vecToCBase32(hash))
|
|
59
65
|
return found === undefined
|
|
60
|
-
?
|
|
61
|
-
: nonEmpty(
|
|
66
|
+
? pureError(ioError({ message: 'not found' }))
|
|
67
|
+
: nonEmpty(found[1], elEmpty())
|
|
62
68
|
},
|
|
63
|
-
write: () => pure(error('write not supported')),
|
|
64
|
-
list: () => pure(entries.map(([h]) => h)),
|
|
69
|
+
write: () => pure(error(ioError({ message: 'write not supported' }))),
|
|
70
|
+
list: () => pure(ok(entries.map(([h]) => h))),
|
|
65
71
|
})
|
|
66
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Runs `e` against the virtual runner and unwraps the effect channel.
|
|
75
|
+
*
|
|
76
|
+
* These proofs drive a runner that implements every operation they use, so a
|
|
77
|
+
* channel failure here is a broken fixture rather than a case under test, and
|
|
78
|
+
* panicking on one is the wanted outcome.
|
|
79
|
+
*
|
|
80
|
+
* The cases that *are* about failure therefore do not come through here: a
|
|
81
|
+
* rejected revision now travels in the same channel as an undispatchable
|
|
82
|
+
* operation, so unwrapping would panic on the thing under test. Those use the
|
|
83
|
+
* raw `virtual` and `assertEvoError`.
|
|
84
|
+
*
|
|
85
|
+
* @type {(state: State) => <T, E>(e: Effect<NodeOp, T, E>) => readonly [State, T]}
|
|
86
|
+
*/
|
|
87
|
+
const virtualOk = state => e => {
|
|
88
|
+
const [s, r] = virtual(state)(e)
|
|
89
|
+
return [s, unwrapResult(r)]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Asserts that `r` failed with an {@link EvoError} whose message contains
|
|
94
|
+
* `expected`.
|
|
95
|
+
*
|
|
96
|
+
* The tag is checked as well as the text, so a `notImplemented` from a runner
|
|
97
|
+
* that quietly lost a handler cannot pass for a rejected revision. Both now
|
|
98
|
+
* arrive through the same channel, which is the point — telling them apart is
|
|
99
|
+
* what the tag is for.
|
|
100
|
+
*
|
|
101
|
+
* @type {(r: Result<unknown, EvoChannel>, expected: string) => void}
|
|
102
|
+
*/
|
|
103
|
+
const assertEvoError = (r, expected) =>
|
|
104
|
+
assert(
|
|
105
|
+
r[0] === 'error' && r[1][0] === 'evoError' && r[1][1].includes(expected),
|
|
106
|
+
['expected an evo error containing', expected, r])
|
|
107
|
+
|
|
67
108
|
export const proof = {
|
|
68
109
|
buildCacheEmptyStoreYieldsEmptyCache: () => {
|
|
69
110
|
const c = fileCas(sha256)(home)
|
|
70
|
-
const [, cache] =
|
|
111
|
+
const [, cache] = virtualOk(emptyState)(buildCache(c))
|
|
71
112
|
assertStructurallySame(cache, emptyCache)
|
|
72
113
|
},
|
|
73
114
|
buildCacheSkipsNonRevisionBlob: () => {
|
|
74
115
|
const c = fileCas(sha256)(home)
|
|
75
116
|
const content = vec8(0x41n) // 'A' — valid UTF-8, not revision JSON
|
|
76
|
-
const [state1] = virtual(emptyState)(c.write(nonEmpty(
|
|
77
|
-
const [, cache] =
|
|
117
|
+
const [state1] = virtual(emptyState)(c.write(nonEmpty(content, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
|
|
118
|
+
const [, cache] = virtualOk(state1)(buildCache(c))
|
|
78
119
|
assertEq(Object.keys(cache.bySubject).length, 0)
|
|
79
120
|
},
|
|
80
121
|
decodeRevisionBlobMissingHashIsNull: () => {
|
|
81
122
|
const c = fileCas(sha256)(home)
|
|
82
|
-
const [, revision] =
|
|
123
|
+
const [, revision] = virtualOk(emptyState)(decodeRevisionBlob(c)(vec8(0x99n)))
|
|
83
124
|
assertEq(revision, null)
|
|
84
125
|
},
|
|
85
126
|
decodeRevisionBlobNonUtf8IsNull: () => {
|
|
86
127
|
const c = fileCas(sha256)(home)
|
|
87
128
|
const oddVec = vec(5n)(0b10101n) // not a whole number of bytes
|
|
88
|
-
const [state1, w] = virtual(emptyState)(c.write(nonEmpty(
|
|
129
|
+
const [state1, w] = virtual(emptyState)(c.write(nonEmpty(oddVec, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
|
|
89
130
|
assert(w[0] === 'ok', ['expected write ok', w])
|
|
90
|
-
const [, revision] =
|
|
131
|
+
const [, revision] = virtualOk(state1)(decodeRevisionBlob(c)(w[1]))
|
|
91
132
|
assertEq(revision, null)
|
|
92
133
|
},
|
|
93
134
|
decodeRevisionBlobInvalidJsonIsNull: () => {
|
|
94
135
|
const c = fileCas(sha256)(home)
|
|
95
136
|
const content = vec8(0x7bn) // '{' alone: valid UTF-8, not parseable JSON
|
|
96
|
-
const [state1, w] = virtual(emptyState)(c.write(nonEmpty(
|
|
137
|
+
const [state1, w] = virtual(emptyState)(c.write(nonEmpty(content, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
|
|
97
138
|
assert(w[0] === 'ok', ['expected write ok', w])
|
|
98
|
-
const [, revision] =
|
|
139
|
+
const [, revision] = virtualOk(state1)(decodeRevisionBlob(c)(w[1]))
|
|
99
140
|
assertEq(revision, null)
|
|
100
141
|
},
|
|
101
142
|
decodeRevisionBlobValidRevisionRoundTrips: () => {
|
|
@@ -104,9 +145,9 @@ export const proof = {
|
|
|
104
145
|
const text = `{"dialect":"${revisionDialect}","subject":"${subjectHash}","parents":[],"snapshot":"${subjectHash}","generation":0}`
|
|
105
146
|
const bytes = tryUtf8(text)
|
|
106
147
|
assert(bytes !== null, 'expected the sample revision text to encode as UTF-8')
|
|
107
|
-
const [state1, w] = virtual(emptyState)(c.write(nonEmpty(
|
|
148
|
+
const [state1, w] = virtual(emptyState)(c.write(nonEmpty(bytes, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
|
|
108
149
|
assert(w[0] === 'ok', ['expected write ok', w])
|
|
109
|
-
const [, revision] =
|
|
150
|
+
const [, revision] = virtualOk(state1)(decodeRevisionBlob(c)(w[1]))
|
|
110
151
|
assert(revision !== null, 'expected a decoded revision')
|
|
111
152
|
assertEq(revision?.subject, subjectHash)
|
|
112
153
|
},
|
|
@@ -120,9 +161,9 @@ export const proof = {
|
|
|
120
161
|
const text = `{"dialect":"${revisionDialect}","subject":"${subjectHash}","parents":[],"snapshot":"${subjectHash}","generation":0}`
|
|
121
162
|
const bytes = tryUtf8(text)
|
|
122
163
|
assert(bytes !== null, 'expected the sample revision text to encode as UTF-8')
|
|
123
|
-
const [state1, w] = virtual(emptyState)(fileCas(sha256)(home).write(nonEmpty(
|
|
164
|
+
const [state1, w] = virtual(emptyState)(fileCas(sha256)(home).write(nonEmpty(bytes, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
|
|
124
165
|
assert(w[0] === 'ok', ['expected write ok', w])
|
|
125
|
-
const [, cache] =
|
|
166
|
+
const [, cache] = virtualOk(state1)(buildCache(c))
|
|
126
167
|
assertEq(cache.bySubject[subjectHash]?.hashes.length, 1)
|
|
127
168
|
assertEq(cache.bySubject[subjectHash]?.hashes[0], vecToCBase32(w[1]))
|
|
128
169
|
},
|
|
@@ -144,8 +185,8 @@ export const proof = {
|
|
|
144
185
|
const childBytes = tryUtf8(childText)
|
|
145
186
|
assert(rootBytes !== null && childBytes !== null, 'expected sample revisions to encode as UTF-8')
|
|
146
187
|
const cas = fixedCas([[childHash, childBytes], [rootHash, rootBytes]])
|
|
147
|
-
const [state0, cacheKey] =
|
|
148
|
-
const [, heads] =
|
|
188
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(cas))
|
|
189
|
+
const [, heads] = virtualOk(state0)(evo(cas)(cacheKey).head('doc'))
|
|
149
190
|
assertEq(heads.length, 1)
|
|
150
191
|
assertEq(heads[0], childCBase32)
|
|
151
192
|
},
|
|
@@ -168,8 +209,8 @@ export const proof = {
|
|
|
168
209
|
const childBytes = tryUtf8(childText)
|
|
169
210
|
assert(rootBytes !== null && childBytes !== null, 'expected sample revisions to encode as UTF-8')
|
|
170
211
|
const cas = fixedCas([[rootHash, rootBytes], [childHash, childBytes]])
|
|
171
|
-
const [state0, cacheKey] =
|
|
172
|
-
const [, heads] =
|
|
212
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(cas))
|
|
213
|
+
const [, heads] = virtualOk(state0)(evo(cas)(cacheKey).head('doc'))
|
|
173
214
|
assertEq(heads.length, 1)
|
|
174
215
|
assertEq(heads[0], vecToCBase32(childHash))
|
|
175
216
|
},
|
|
@@ -179,38 +220,35 @@ export const proof = {
|
|
|
179
220
|
// set), and a keep (an unrelated existing head surviving the fold).
|
|
180
221
|
addRevisionBuildsHeadsAcrossChainAndFork: () => {
|
|
181
222
|
const c = fileCas(sha256)(home)
|
|
182
|
-
const [state0, cacheKey] =
|
|
223
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
183
224
|
const e = evo(c)(cacheKey)
|
|
184
225
|
const snapshotHash = vecToCBase32(vec8(0x22n))
|
|
185
226
|
|
|
186
227
|
/** @type {RevisionData} */
|
|
187
228
|
const rev0 = { parents: [], subject: 'doc', snapshot: snapshotHash }
|
|
188
|
-
const [state1, rev0Result] =
|
|
189
|
-
|
|
190
|
-
const rev0Hash = rev0Result[1]
|
|
229
|
+
const [state1, rev0Result] = virtualOk(state0)(e.add(rev0))
|
|
230
|
+
const rev0Hash = rev0Result
|
|
191
231
|
|
|
192
232
|
// rev1 inherits rev0's snapshot; rev2 states a different one — so the
|
|
193
233
|
// two concurrent children are genuinely distinct blobs (both children
|
|
194
234
|
// of rev0, same generation), not one deduplicated revision.
|
|
195
235
|
/** @type {RevisionData} */
|
|
196
236
|
const rev1 = { parents: [rev0Hash], subject: 'doc' }
|
|
197
|
-
const [state2, rev1Result] =
|
|
198
|
-
|
|
199
|
-
const rev1Hash = rev1Result[1]
|
|
237
|
+
const [state2, rev1Result] = virtualOk(state1)(e.add(rev1))
|
|
238
|
+
const rev1Hash = rev1Result
|
|
200
239
|
|
|
201
240
|
/** @type {RevisionData} */
|
|
202
241
|
const rev2 = { parents: [rev0Hash], subject: 'doc', snapshot: vecToCBase32(vec8(0x23n)) }
|
|
203
|
-
const [state3, rev2Result] =
|
|
204
|
-
|
|
205
|
-
const rev2Hash = rev2Result[1]
|
|
242
|
+
const [state3, rev2Result] = virtualOk(state2)(e.add(rev2))
|
|
243
|
+
const rev2Hash = rev2Result
|
|
206
244
|
|
|
207
|
-
const [state4, heads] =
|
|
245
|
+
const [state4, heads] = virtualOk(state3)(e.head('doc'))
|
|
208
246
|
assertEq(heads.length, 2)
|
|
209
247
|
assert(heads.includes(rev1Hash), ['expected rev1 to remain a head', heads])
|
|
210
248
|
assert(heads.includes(rev2Hash), ['expected rev2 to be a head', heads])
|
|
211
249
|
assert(!heads.includes(rev0Hash), ['expected rev0 to no longer be a head', heads])
|
|
212
250
|
|
|
213
|
-
const [, subjects] =
|
|
251
|
+
const [, subjects] = virtualOk(state4)(e.list())
|
|
214
252
|
assertEq(subjects.length, 1)
|
|
215
253
|
assertEq(subjects[0], 'doc')
|
|
216
254
|
},
|
|
@@ -220,16 +258,14 @@ export const proof = {
|
|
|
220
258
|
// and neither subject appears in the other's result.
|
|
221
259
|
listPartitionsSubjectsByHeadArchivedFlag: () => {
|
|
222
260
|
const c = fileCas(sha256)(home)
|
|
223
|
-
const [state0, cacheKey] =
|
|
261
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
224
262
|
const e = evo(c)(cacheKey)
|
|
225
|
-
const [state1, live] =
|
|
226
|
-
|
|
227
|
-
const [
|
|
228
|
-
assert(gone[0] === 'ok', ['expected the archived add to succeed', gone])
|
|
229
|
-
const [state3, active] = virtual(state2)(e.list())
|
|
263
|
+
const [state1, live] = virtualOk(state0)(e.add({ parents: [], subject: 'live', snapshot: vecToCBase32(vec8(0x40n)) }))
|
|
264
|
+
const [state2, gone] = virtualOk(state1)(e.add({ parents: [], subject: 'gone', snapshot: vecToCBase32(vec8(0x41n)), archived: true }))
|
|
265
|
+
const [state3, active] = virtualOk(state2)(e.list())
|
|
230
266
|
assertEq(active.length, 1)
|
|
231
267
|
assertEq(active[0], 'live')
|
|
232
|
-
const [, archived] =
|
|
268
|
+
const [, archived] = virtualOk(state3)(e.list(true))
|
|
233
269
|
assertEq(archived.length, 1)
|
|
234
270
|
assertEq(archived[0], 'gone')
|
|
235
271
|
},
|
|
@@ -238,22 +274,19 @@ export const proof = {
|
|
|
238
274
|
// — so the subject stays out of the archived-only result.
|
|
239
275
|
listTreatsDisagreeingHeadsAsActive: () => {
|
|
240
276
|
const c = fileCas(sha256)(home)
|
|
241
|
-
const [state0, cacheKey] =
|
|
277
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
242
278
|
const e = evo(c)(cacheKey)
|
|
243
|
-
const [state1, root] =
|
|
244
|
-
assert(root[0] === 'ok', ['expected root ok', root])
|
|
279
|
+
const [state1, root] = virtualOk(state0)(e.add({ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x42n)) }))
|
|
245
280
|
// The two children differ only in `archived`, which is enough to make
|
|
246
281
|
// them distinct blobs, hence two concurrent heads of one root.
|
|
247
|
-
const [state2
|
|
248
|
-
|
|
249
|
-
const [
|
|
250
|
-
assert(dropped[0] === 'ok', ['expected the archived child ok', dropped])
|
|
251
|
-
const [state4, heads] = virtual(state3)(e.head('doc'))
|
|
282
|
+
const [state2] = virtualOk(state1)(e.add({ parents: [root], subject: 'doc' }))
|
|
283
|
+
const [state3] = virtualOk(state2)(e.add({ parents: [root], subject: 'doc', archived: true }))
|
|
284
|
+
const [state4, heads] = virtualOk(state3)(e.head('doc'))
|
|
252
285
|
assertEq(heads.length, 2)
|
|
253
|
-
const [state5, active] =
|
|
286
|
+
const [state5, active] = virtualOk(state4)(e.list())
|
|
254
287
|
assertEq(active.length, 1)
|
|
255
288
|
assertEq(active[0], 'doc')
|
|
256
|
-
const [, archived] =
|
|
289
|
+
const [, archived] = virtualOk(state5)(e.list(true))
|
|
257
290
|
assertEq(archived.length, 0)
|
|
258
291
|
},
|
|
259
292
|
// `archived` is a property of a revision, not of a subject: a subject
|
|
@@ -261,16 +294,14 @@ export const proof = {
|
|
|
261
294
|
// demotes that revision out of the head set.
|
|
262
295
|
listIgnoresArchivedRevisionsThatAreNoLongerHeads: () => {
|
|
263
296
|
const c = fileCas(sha256)(home)
|
|
264
|
-
const [state0, cacheKey] =
|
|
297
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
265
298
|
const e = evo(c)(cacheKey)
|
|
266
|
-
const [state1, root] =
|
|
267
|
-
|
|
268
|
-
const [
|
|
269
|
-
assert(revived[0] === 'ok', ['expected the unarchived child ok', revived])
|
|
270
|
-
const [state3, active] = virtual(state2)(e.list())
|
|
299
|
+
const [state1, root] = virtualOk(state0)(e.add({ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x45n)), archived: true }))
|
|
300
|
+
const [state2] = virtualOk(state1)(e.add({ parents: [root], subject: 'doc' }))
|
|
301
|
+
const [state3, active] = virtualOk(state2)(e.list())
|
|
271
302
|
assertEq(active.length, 1)
|
|
272
303
|
assertEq(active[0], 'doc')
|
|
273
|
-
const [, archived] =
|
|
304
|
+
const [, archived] = virtualOk(state3)(e.list(true))
|
|
274
305
|
assertEq(archived.length, 0)
|
|
275
306
|
},
|
|
276
307
|
// A subject can end up with no current head at all, and a status is a
|
|
@@ -286,29 +317,27 @@ export const proof = {
|
|
|
286
317
|
const bytes = tryUtf8(text)
|
|
287
318
|
assert(bytes !== null, 'expected the sample revision text to encode as UTF-8')
|
|
288
319
|
const cas = fixedCas([[selfHash, bytes]])
|
|
289
|
-
const [state0, cacheKey] =
|
|
320
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(cas))
|
|
290
321
|
const e = evo(cas)(cacheKey)
|
|
291
|
-
const [state1, heads] =
|
|
322
|
+
const [state1, heads] = virtualOk(state0)(e.head('doc'))
|
|
292
323
|
assertEq(heads.length, 0)
|
|
293
|
-
const [state2, active] =
|
|
324
|
+
const [state2, active] = virtualOk(state1)(e.list())
|
|
294
325
|
assertEq(active.length, 0)
|
|
295
|
-
const [, archived] =
|
|
326
|
+
const [, archived] = virtualOk(state2)(e.list(true))
|
|
296
327
|
assertEq(archived.length, 0)
|
|
297
328
|
},
|
|
298
329
|
// Adding the exact same revision twice yields the same (deduplicated)
|
|
299
330
|
// content hash and must not duplicate the head entry.
|
|
300
331
|
addRevisionIdempotentOnDuplicateContent: () => {
|
|
301
332
|
const c = fileCas(sha256)(home)
|
|
302
|
-
const [state0, cacheKey] =
|
|
333
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
303
334
|
const e = evo(c)(cacheKey)
|
|
304
335
|
/** @type {RevisionData} */
|
|
305
336
|
const input = { parents: [], subject: 'x', snapshot: vecToCBase32(vec8(0x33n)) }
|
|
306
|
-
const [state1, r1] =
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
assertEq(r1[1], r2[1])
|
|
311
|
-
const [, heads] = virtual(state2)(e.head('x'))
|
|
337
|
+
const [state1, r1] = virtualOk(state0)(e.add(input))
|
|
338
|
+
const [state2, r2] = virtualOk(state1)(e.add(input))
|
|
339
|
+
assertEq(r1, r2)
|
|
340
|
+
const [, heads] = virtualOk(state2)(e.head('x'))
|
|
312
341
|
assertEq(heads.length, 1)
|
|
313
342
|
},
|
|
314
343
|
// Two `add` calls describing the same logical revision but spelling the
|
|
@@ -318,44 +347,39 @@ export const proof = {
|
|
|
318
347
|
// `canonicalHash` exists to prevent.
|
|
319
348
|
addRevisionCanonicalizesParentSpellingBeforeSerializing: () => {
|
|
320
349
|
const c = fileCas(sha256)(home)
|
|
321
|
-
const [state0, cacheKey] =
|
|
350
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
322
351
|
const e = evo(c)(cacheKey)
|
|
323
352
|
/** @type {RevisionData} */
|
|
324
353
|
const root = { parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x2en)) }
|
|
325
|
-
const [state1, rootResult] =
|
|
326
|
-
|
|
327
|
-
const rootHash = rootResult[1]
|
|
354
|
+
const [state1, rootResult] = virtualOk(state0)(e.add(root))
|
|
355
|
+
const rootHash = rootResult
|
|
328
356
|
assert(rootHash !== rootHash.toUpperCase(), 'expected the sample hash to contain letters')
|
|
329
357
|
/** @type {RevisionData} */
|
|
330
358
|
const child1 = { parents: [rootHash], subject: 'doc' }
|
|
331
|
-
const [state2, child1Result] =
|
|
332
|
-
assert(child1Result[0] === 'ok', ['expected child1 ok', child1Result])
|
|
359
|
+
const [state2, child1Result] = virtualOk(state1)(e.add(child1))
|
|
333
360
|
/** @type {RevisionData} */
|
|
334
361
|
const child2 = { parents: [rootHash.toUpperCase()], subject: 'doc' }
|
|
335
|
-
const [state3, child2Result] =
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const [, heads] = virtual(state3)(e.head('doc'))
|
|
362
|
+
const [state3, child2Result] = virtualOk(state2)(e.add(child2))
|
|
363
|
+
assertEq(child1Result, child2Result)
|
|
364
|
+
const [, heads] = virtualOk(state3)(e.head('doc'))
|
|
339
365
|
assertEq(heads.length, 1)
|
|
340
|
-
assertEq(heads[0], child1Result
|
|
366
|
+
assertEq(heads[0], child1Result)
|
|
341
367
|
},
|
|
342
368
|
// `subject` omitted with exactly one parent is inherited from that
|
|
343
369
|
// parent's own `subject`.
|
|
344
370
|
addRevisionResolvesSubjectFromSingleParent: () => {
|
|
345
371
|
const c = fileCas(sha256)(home)
|
|
346
|
-
const [state0, cacheKey] =
|
|
372
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
347
373
|
const e = evo(c)(cacheKey)
|
|
348
374
|
/** @type {RevisionData} */
|
|
349
375
|
const root = { parents: [], subject: 'inherit-me', snapshot: vecToCBase32(vec8(0x44n)) }
|
|
350
|
-
const [state1, rootResult] =
|
|
351
|
-
|
|
352
|
-
const rootHash = rootResult[1]
|
|
376
|
+
const [state1, rootResult] = virtualOk(state0)(e.add(root))
|
|
377
|
+
const rootHash = rootResult
|
|
353
378
|
/** @type {RevisionData} */
|
|
354
379
|
const child = { parents: [rootHash] }
|
|
355
|
-
const [state2, childResult] =
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
const [, heads] = virtual(state2)(e.head('inherit-me'))
|
|
380
|
+
const [state2, childResult] = virtualOk(state1)(e.add(child))
|
|
381
|
+
const childHash = childResult
|
|
382
|
+
const [, heads] = virtualOk(state2)(e.head('inherit-me'))
|
|
359
383
|
assertEq(heads.length, 1)
|
|
360
384
|
assertEq(heads[0], childHash)
|
|
361
385
|
},
|
|
@@ -365,21 +389,19 @@ export const proof = {
|
|
|
365
389
|
// inherits the parent's stored snapshot and gets `generation` 1.
|
|
366
390
|
addComputesGenerationAndResolvesSnapshot: () => {
|
|
367
391
|
const c = fileCas(sha256)(home)
|
|
368
|
-
const [state0, cacheKey] =
|
|
392
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
369
393
|
const e = evo(c)(cacheKey)
|
|
370
394
|
const subjectHash = vecToCBase32(vec8(0x71n))
|
|
371
|
-
const [state1, rootResult] =
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
const [state2, root] = virtual(state1)(decodeRevisionBlob(c)(rootHashVec))
|
|
395
|
+
const [state1, rootResult] = virtualOk(state0)(e.add({ parents: [], subject: subjectHash }))
|
|
396
|
+
const rootHashVec = unwrap(cBase32ToVec(rootResult))
|
|
397
|
+
const [state2, root] = virtualOk(state1)(decodeRevisionBlob(c)(rootHashVec))
|
|
375
398
|
assert(root !== null, 'expected the stored root to decode')
|
|
376
399
|
assertEq(root?.generation, 0)
|
|
377
400
|
assertEq(root?.snapshot, subjectHash)
|
|
378
401
|
|
|
379
|
-
const [state3, childResult] =
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
const [, child] = virtual(state3)(decodeRevisionBlob(c)(childHashVec))
|
|
402
|
+
const [state3, childResult] = virtualOk(state2)(e.add({ parents: [rootResult], subject: subjectHash }))
|
|
403
|
+
const childHashVec = unwrap(cBase32ToVec(childResult))
|
|
404
|
+
const [, child] = virtualOk(state3)(decodeRevisionBlob(c)(childHashVec))
|
|
383
405
|
assert(child !== null, 'expected the stored child to decode')
|
|
384
406
|
assertEq(child?.generation, 1)
|
|
385
407
|
assertEq(child?.snapshot, subjectHash)
|
|
@@ -388,24 +410,19 @@ export const proof = {
|
|
|
388
410
|
// and one at generation 1 yield generation 3.
|
|
389
411
|
addComputesMergeGenerationFromMaxOfParents: () => {
|
|
390
412
|
const c = fileCas(sha256)(home)
|
|
391
|
-
const [state0, cacheKey] =
|
|
413
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
392
414
|
const e = evo(c)(cacheKey)
|
|
393
415
|
const snap = vecToCBase32(vec8(0x72n))
|
|
394
416
|
// Mainline chain: root(gen0) → a(gen1) → b(gen2).
|
|
395
|
-
const [state1, root] =
|
|
396
|
-
|
|
397
|
-
const [
|
|
398
|
-
assert(a[0] === 'ok', ['expected a ok', a])
|
|
399
|
-
const [state3, b] = virtual(state2)(e.add({ parents: [a[1]], subject: 'm', snapshot: snap }))
|
|
400
|
-
assert(b[0] === 'ok', ['expected b ok', b])
|
|
417
|
+
const [state1, root] = virtualOk(state0)(e.add({ parents: [], subject: 'm', snapshot: snap }))
|
|
418
|
+
const [state2, a] = virtualOk(state1)(e.add({ parents: [root], subject: 'm', snapshot: snap }))
|
|
419
|
+
const [state3, b] = virtualOk(state2)(e.add({ parents: [a], subject: 'm', snapshot: snap }))
|
|
401
420
|
// Side branch off root: c(gen1).
|
|
402
|
-
const [state4, cRev] =
|
|
403
|
-
assert(cRev[0] === 'ok', ['expected c ok', cRev])
|
|
421
|
+
const [state4, cRev] = virtualOk(state3)(e.add({ parents: [root], subject: 'm', snapshot: snap }))
|
|
404
422
|
// Merge of b(gen2) and c(gen1) → gen3.
|
|
405
|
-
const [state5, merge] =
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
const [, mergeRev] = virtual(state5)(decodeRevisionBlob(c)(mergeHashVec))
|
|
423
|
+
const [state5, merge] = virtualOk(state4)(e.add({ parents: [b, cRev], subject: 'm', snapshot: snap }))
|
|
424
|
+
const mergeHashVec = unwrap(cBase32ToVec(merge))
|
|
425
|
+
const [, mergeRev] = virtualOk(state5)(decodeRevisionBlob(c)(mergeHashVec))
|
|
409
426
|
assert(mergeRev !== null, 'expected the stored merge to decode')
|
|
410
427
|
assertEq(mergeRev?.generation, 3)
|
|
411
428
|
},
|
|
@@ -415,25 +432,22 @@ export const proof = {
|
|
|
415
432
|
// byte-identical to the same `add` without it.
|
|
416
433
|
addIgnoresSuppliedGeneration: () => {
|
|
417
434
|
const c = fileCas(sha256)(home)
|
|
418
|
-
const [state0, cacheKey] =
|
|
435
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
419
436
|
const e = evo(c)(cacheKey)
|
|
420
437
|
const subjectHash = vecToCBase32(vec8(0x73n))
|
|
421
|
-
const [state1, claimed] =
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
assertEq(claimed[1], plain[1])
|
|
426
|
-
const [, revision] = virtual(state2)(e.revision(claimed[1]))
|
|
438
|
+
const [state1, claimed] = virtualOk(state0)(e.add({ parents: [], subject: subjectHash, generation: 42 }))
|
|
439
|
+
const [state2, plain] = virtualOk(state1)(e.add({ parents: [], subject: subjectHash }))
|
|
440
|
+
assertEq(claimed, plain)
|
|
441
|
+
const [, revision] = virtual(state2)(e.revision(claimed))
|
|
427
442
|
assert(revision[0] === 'ok', ['expected revision ok', revision])
|
|
428
443
|
assertEq(revision[1].generation, 0)
|
|
429
444
|
},
|
|
430
445
|
addRevisionSubjectRequiredForZeroParents: () => {
|
|
431
446
|
const c = fileCas(sha256)(home)
|
|
432
|
-
const [state0, cacheKey] =
|
|
447
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
433
448
|
const e = evo(c)(cacheKey)
|
|
434
449
|
const [, result] = virtual(state0)(e.add({ parents: [] }))
|
|
435
|
-
|
|
436
|
-
assert(result[0] === 'error' && result[1].includes('subject is required'), ['unexpected message', result])
|
|
450
|
+
assertEvoError(result, 'subject is required')
|
|
437
451
|
},
|
|
438
452
|
// Zero parents and no input `snapshot`: `resolveSnapshot` falls back to
|
|
439
453
|
// `subject` as the reference, which must itself be a hash. A subject with
|
|
@@ -442,40 +456,36 @@ export const proof = {
|
|
|
442
456
|
// an explicit `snapshot`.
|
|
443
457
|
addRevisionNonHashSubjectWithoutSnapshotIsError: () => {
|
|
444
458
|
const c = fileCas(sha256)(home)
|
|
445
|
-
const [state0, cacheKey] =
|
|
459
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
446
460
|
const e = evo(c)(cacheKey)
|
|
447
461
|
const [, result] = virtual(state0)(e.add({ parents: [], subject: 'not-a-hash!' }))
|
|
448
|
-
|
|
449
|
-
assert(result[0] === 'error' && result[1].includes('subject must be a valid hash'), ['unexpected message', result])
|
|
462
|
+
assertEvoError(result, 'subject must be a valid hash')
|
|
450
463
|
},
|
|
451
464
|
addRevisionInvalidParentHashIsError: () => {
|
|
452
465
|
const c = fileCas(sha256)(home)
|
|
453
|
-
const [state0, cacheKey] =
|
|
466
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
454
467
|
const e = evo(c)(cacheKey)
|
|
455
468
|
const [, result] = virtual(state0)(e.add({ parents: ['not a valid cbase32!'] }))
|
|
456
|
-
|
|
457
|
-
assert(result[0] === 'error' && result[1].includes('invalid parent hash'), ['unexpected message', result])
|
|
469
|
+
assertEvoError(result, 'invalid parent hash')
|
|
458
470
|
},
|
|
459
471
|
addRevisionParentNotARevisionIsError: () => {
|
|
460
472
|
const c = fileCas(sha256)(home)
|
|
461
|
-
const [state0, cacheKey] =
|
|
473
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
462
474
|
const e = evo(c)(cacheKey)
|
|
463
475
|
const missingParent = vecToCBase32(vec8(0x55n))
|
|
464
476
|
const [, result] = virtual(state0)(e.add({ parents: [missingParent] }))
|
|
465
|
-
|
|
466
|
-
assert(result[0] === 'error' && result[1].includes('parent is not a revision blob'), ['unexpected message', result])
|
|
477
|
+
assertEvoError(result, 'parent is not a revision blob')
|
|
467
478
|
},
|
|
468
479
|
// An explicit `subject` must not bypass parent validation: a missing
|
|
469
480
|
// parent is still an error even though `subject` alone would otherwise
|
|
470
481
|
// resolve without looking at any parent.
|
|
471
482
|
addRevisionValidatesParentsEvenWithExplicitSubject: () => {
|
|
472
483
|
const c = fileCas(sha256)(home)
|
|
473
|
-
const [state0, cacheKey] =
|
|
484
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
474
485
|
const e = evo(c)(cacheKey)
|
|
475
486
|
const missingParent = vecToCBase32(vec8(0x66n))
|
|
476
487
|
const [, result] = virtual(state0)(e.add({ parents: [missingParent], subject: 'doc' }))
|
|
477
|
-
|
|
478
|
-
assert(result[0] === 'error' && result[1].includes('parent is not a revision blob'), ['unexpected message', result])
|
|
488
|
+
assertEvoError(result, 'parent is not a revision blob')
|
|
479
489
|
},
|
|
480
490
|
// A revision models one step in the evolution of a single subject: a
|
|
481
491
|
// parent that actually belongs to a *different* subject must be
|
|
@@ -485,27 +495,24 @@ export const proof = {
|
|
|
485
495
|
// never remove the parent from its real subject's head set.
|
|
486
496
|
addRevisionRejectsCrossSubjectParent: () => {
|
|
487
497
|
const c = fileCas(sha256)(home)
|
|
488
|
-
const [state0, cacheKey] =
|
|
498
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
489
499
|
const e = evo(c)(cacheKey)
|
|
490
500
|
/** @type {RevisionData} */
|
|
491
501
|
const rootA = { parents: [], subject: 'A', snapshot: vecToCBase32(vec8(0x67n)) }
|
|
492
|
-
const [state1, rootAResult] =
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
assertEq(result[0], 'error')
|
|
496
|
-
assert(result[0] === 'error' && result[1].includes('different subject'), ['unexpected message', result])
|
|
502
|
+
const [state1, rootAResult] = virtualOk(state0)(e.add(rootA))
|
|
503
|
+
const [, result] = virtual(state1)(e.add({ parents: [rootAResult], subject: 'B' }))
|
|
504
|
+
assertEvoError(result, 'different subject')
|
|
497
505
|
},
|
|
498
506
|
// With multiple parents, a failure on an earlier one short-circuits the
|
|
499
507
|
// fold — the second (here, a well-formed but nonexistent) parent is
|
|
500
508
|
// never even looked up.
|
|
501
509
|
addRevisionShortCircuitsOnFirstInvalidParentAmongMultiple: () => {
|
|
502
510
|
const c = fileCas(sha256)(home)
|
|
503
|
-
const [state0, cacheKey] =
|
|
511
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
504
512
|
const e = evo(c)(cacheKey)
|
|
505
513
|
const missingParent = vecToCBase32(vec8(0x99n))
|
|
506
514
|
const [, result] = virtual(state0)(e.add({ parents: ['not a valid cbase32!', missingParent], subject: 'doc' }))
|
|
507
|
-
|
|
508
|
-
assert(result[0] === 'error' && result[1].includes('invalid parent hash'), ['unexpected message', result])
|
|
515
|
+
assertEvoError(result, 'invalid parent hash')
|
|
509
516
|
},
|
|
510
517
|
// Two *valid* parents without an explicit `snapshot` cannot resolve one
|
|
511
518
|
// (no single parent snapshot to inherit) — a distinct failure from a
|
|
@@ -513,37 +520,35 @@ export const proof = {
|
|
|
513
520
|
// boundary now that the format requires an explicit `snapshot`.
|
|
514
521
|
addRevisionInvalidReferencesIsError: () => {
|
|
515
522
|
const c = fileCas(sha256)(home)
|
|
516
|
-
const [state0, cacheKey] =
|
|
523
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
517
524
|
const e = evo(c)(cacheKey)
|
|
518
525
|
/** @type {RevisionData} */
|
|
519
526
|
const rootA = { parents: [], subject: 'merge', snapshot: vecToCBase32(vec8(0x77n)) }
|
|
520
|
-
const [state1, rootAResult] =
|
|
521
|
-
assert(rootAResult[0] === 'ok', ['expected rootA ok', rootAResult])
|
|
527
|
+
const [state1, rootAResult] = virtualOk(state0)(e.add(rootA))
|
|
522
528
|
/** @type {RevisionData} */
|
|
523
529
|
const rootB = { parents: [], subject: 'merge', snapshot: vecToCBase32(vec8(0x88n)) }
|
|
524
|
-
const [state2, rootBResult] =
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
530
|
+
const [state2, rootBResult] = virtualOk(state1)(e.add(rootB))
|
|
531
|
+
const [, result] = virtual(state2)(e.add({ parents: [rootAResult, rootBResult], subject: 'merge' }))
|
|
532
|
+
assert(
|
|
533
|
+
result[0] === 'error' && result[1][0] === 'evoError'
|
|
534
|
+
&& !result[1][1].includes('parent is not a revision blob'),
|
|
535
|
+
['unexpected message', result])
|
|
529
536
|
},
|
|
530
537
|
addRevisionTooLargeToEncodeIsError: () => {
|
|
531
538
|
const c = fileCas(sha256)(home)
|
|
532
|
-
const [state0, cacheKey] =
|
|
539
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
533
540
|
const e = evo(c)(cacheKey)
|
|
534
541
|
const hugeSubject = 'x'.repeat(200_000)
|
|
535
542
|
/** @type {RevisionData} */
|
|
536
543
|
const input = { parents: [], subject: hugeSubject, snapshot: vecToCBase32(vec8(0x88n)) }
|
|
537
544
|
const [, result] = virtual(state0)(e.add(input))
|
|
538
|
-
|
|
539
|
-
assert(result[0] === 'error' && result[1] === 'revision too large to encode', ['unexpected message', result])
|
|
545
|
+
assertEvoError(result, 'revision too large to encode')
|
|
540
546
|
},
|
|
541
547
|
addRevisionWriteFailureIsError: () => {
|
|
542
|
-
const [, cacheKey] =
|
|
548
|
+
const [, cacheKey] = virtualOk(emptyState)(initEvo(writeFailingCas))
|
|
543
549
|
const e = evo(writeFailingCas)(cacheKey)
|
|
544
550
|
const [, result] = virtual(emptyState)(e.add({ parents: [], subject: vecToCBase32(vec8(0x99n)) }))
|
|
545
|
-
|
|
546
|
-
assert(result[0] === 'error' && result[1] === 'failed to write revision to CAS', ['unexpected message', result])
|
|
551
|
+
assertEvoError(result, 'failed to write revision to CAS')
|
|
547
552
|
},
|
|
548
553
|
// Every way `revision` can fail is its own message, not one `null`: an
|
|
549
554
|
// undecodable hash, a hash the store has nothing under, a read that failed
|
|
@@ -552,41 +557,37 @@ export const proof = {
|
|
|
552
557
|
// stores of arbitrary content); this read keeps them apart.
|
|
553
558
|
revisionInvalidHashIsError: () => {
|
|
554
559
|
const c = fileCas(sha256)(home)
|
|
555
|
-
const [state0, cacheKey] =
|
|
560
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
556
561
|
const e = evo(c)(cacheKey)
|
|
557
562
|
const [, result] = virtual(state0)(e.revision('not a valid cbase32!'))
|
|
558
|
-
|
|
559
|
-
assert(result[0] === 'error' && result[1].includes('invalid hash'), ['unexpected message', result])
|
|
563
|
+
assertEvoError(result, 'invalid hash')
|
|
560
564
|
},
|
|
561
565
|
revisionMissingHashIsError: () => {
|
|
562
566
|
const c = fileCas(sha256)(home)
|
|
563
|
-
const [state0, cacheKey] =
|
|
567
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
564
568
|
const e = evo(c)(cacheKey)
|
|
565
569
|
const [, result] = virtual(state0)(e.revision(vecToCBase32(vec8(0x9an))))
|
|
566
|
-
|
|
567
|
-
assert(result[0] === 'error' && result[1].includes('revision not found'), ['unexpected message', result])
|
|
570
|
+
assertEvoError(result, 'revision not found')
|
|
568
571
|
},
|
|
569
572
|
// Regression: a read failure that is not a missing shard — a permission or
|
|
570
573
|
// mid-stream I/O error, or a blob too large to buffer into one `Vec` —
|
|
571
574
|
// must not be reported as "not found". The blob may well be there; saying
|
|
572
575
|
// it is absent would be a false answer, not merely a vague one.
|
|
573
576
|
revisionReadFailureIsNotReportedAsMissing: () => {
|
|
574
|
-
const [state0, cacheKey] =
|
|
577
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(readFailingCas))
|
|
575
578
|
const e = evo(readFailingCas)(cacheKey)
|
|
576
579
|
const [, result] = virtual(state0)(e.revision(vecToCBase32(vec8(0x9bn))))
|
|
577
|
-
|
|
578
|
-
assert(result[0] === 'error' && result[1].includes('failed to read revision'), ['unexpected message', result])
|
|
580
|
+
assertEvoError(result, 'failed to read revision')
|
|
579
581
|
},
|
|
580
582
|
revisionNonRevisionBlobIsError: () => {
|
|
581
583
|
const c = fileCas(sha256)(home)
|
|
582
|
-
const [state0, cacheKey] =
|
|
584
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
583
585
|
const e = evo(c)(cacheKey)
|
|
584
586
|
const content = vec8(0x41n) // 'A' — valid UTF-8, not revision JSON
|
|
585
|
-
const [state1, w] = virtual(state0)(c.write(nonEmpty(
|
|
587
|
+
const [state1, w] = virtual(state0)(c.write(nonEmpty(content, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
|
|
586
588
|
assert(w[0] === 'ok', ['expected write ok', w])
|
|
587
589
|
const [, result] = virtual(state1)(e.revision(vecToCBase32(w[1])))
|
|
588
|
-
|
|
589
|
-
assert(result[0] === 'error' && result[1].includes('not a revision blob'), ['unexpected message', result])
|
|
590
|
+
assertEvoError(result, 'not a revision blob')
|
|
590
591
|
},
|
|
591
592
|
// A blob written by somebody else can spell its references in any accepted
|
|
592
593
|
// cbase32 alias (case, `i`/`l`/`o`). What `revision` returns is
|
|
@@ -594,7 +595,7 @@ export const proof = {
|
|
|
594
595
|
// directly instead of knowing about cbase32 aliasing.
|
|
595
596
|
revisionCanonicalizesReferenceSpellings: () => {
|
|
596
597
|
const c = fileCas(sha256)(home)
|
|
597
|
-
const [state0, cacheKey] =
|
|
598
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
598
599
|
const e = evo(c)(cacheKey)
|
|
599
600
|
const parentCanonical = vecToCBase32(vec8(0xffn))
|
|
600
601
|
const snapshotCanonical = vecToCBase32(vec8(0xfen))
|
|
@@ -606,7 +607,7 @@ export const proof = {
|
|
|
606
607
|
const text = `{"dialect":"${revisionDialect}","subject":"doc","parents":["${parentAlias}"],"snapshot":"${snapshotAlias}","generation":1}`
|
|
607
608
|
const bytes = tryUtf8(text)
|
|
608
609
|
assert(bytes !== null, 'expected the sample revision text to encode as UTF-8')
|
|
609
|
-
const [state1, w] = virtual(state0)(c.write(nonEmpty(
|
|
610
|
+
const [state1, w] = virtual(state0)(c.write(nonEmpty(bytes, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
|
|
610
611
|
assert(w[0] === 'ok', ['expected write ok', w])
|
|
611
612
|
const [, result] = virtual(state1)(e.revision(vecToCBase32(w[1])))
|
|
612
613
|
assert(result[0] === 'ok', ['expected revision ok', result])
|
|
@@ -621,78 +622,160 @@ export const proof = {
|
|
|
621
622
|
// can be added again unchanged — same content, same hash.
|
|
622
623
|
revisionRoundTripsThroughAdd: () => {
|
|
623
624
|
const c = fileCas(sha256)(home)
|
|
624
|
-
const [state0, cacheKey] =
|
|
625
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
625
626
|
const e = evo(c)(cacheKey)
|
|
626
627
|
const subjectHash = vecToCBase32(vec8(0x74n))
|
|
627
|
-
const [state1, root] =
|
|
628
|
-
|
|
629
|
-
const [
|
|
630
|
-
assert(child[0] === 'ok', ['expected child ok', child])
|
|
631
|
-
const [state3, result] = virtual(state2)(e.revision(child[1]))
|
|
628
|
+
const [state1, root] = virtualOk(state0)(e.add({ parents: [], subject: subjectHash }))
|
|
629
|
+
const [state2, child] = virtualOk(state1)(e.add({ parents: [root], archived: true }))
|
|
630
|
+
const [state3, result] = virtual(state2)(e.revision(child))
|
|
632
631
|
assert(result[0] === 'ok', ['expected revision ok', result])
|
|
633
632
|
assertEq(result[1].subject, subjectHash)
|
|
634
633
|
assertEq(result[1].snapshot, subjectHash)
|
|
635
634
|
assertEq(result[1].generation, 1)
|
|
636
635
|
assertEq(result[1].archived, true)
|
|
637
636
|
assertEq(result[1].parents.length, 1)
|
|
638
|
-
assertEq(result[1].parents[0], root
|
|
639
|
-
const [, readded] =
|
|
640
|
-
|
|
641
|
-
assertEq(readded[1], child[1])
|
|
637
|
+
assertEq(result[1].parents[0], root)
|
|
638
|
+
const [, readded] = virtualOk(state3)(e.add(result[1]))
|
|
639
|
+
assertEq(readded, child)
|
|
642
640
|
},
|
|
643
641
|
revisionLockRoundTripsAndCanonicalizes: () => {
|
|
644
642
|
const c = fileCas(sha256)(home)
|
|
645
|
-
const [state0, cacheKey] =
|
|
643
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
646
644
|
const e = evo(c)(cacheKey)
|
|
647
645
|
const canonical = vecToCBase32(vec8(0xffn))
|
|
648
646
|
const alias = canonical.toUpperCase()
|
|
649
|
-
const [state1, added] =
|
|
647
|
+
const [state1, added] = virtualOk(state0)(e.add({
|
|
650
648
|
parents: [], subject: 'doc', snapshot: canonical,
|
|
651
649
|
lock: { dependency: alias },
|
|
652
650
|
}))
|
|
653
|
-
|
|
654
|
-
|
|
651
|
+
const [, result] = virtual(state1)(e.revision(added))
|
|
652
|
+
assert(result[0] === 'ok', ['expected revision ok', result])
|
|
653
|
+
const lock = result[1].lock
|
|
654
|
+
assert(typeof lock === 'object', ['expected an inline lock map', lock])
|
|
655
|
+
assertEq(lock.dependency, canonical)
|
|
656
|
+
},
|
|
657
|
+
// A `lock` naming a `vnd.fjs.lock` blob is stored and read back as the
|
|
658
|
+
// reference it is: validated as a hash and re-spelled canonically, like
|
|
659
|
+
// `snapshot`, and never followed — resolving one is a resolver's job.
|
|
660
|
+
revisionSharedLockReferenceRoundTripsAndCanonicalizes: () => {
|
|
661
|
+
const c = fileCas(sha256)(home)
|
|
662
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
663
|
+
const e = evo(c)(cacheKey)
|
|
664
|
+
const canonical = vecToCBase32(vec8(0xffn))
|
|
665
|
+
const alias = canonical.toUpperCase()
|
|
666
|
+
const [state1, added] = virtualOk(state0)(e.add({
|
|
667
|
+
parents: [], subject: 'doc', snapshot: canonical, lock: alias,
|
|
668
|
+
}))
|
|
669
|
+
const [state2, result] = virtual(state1)(e.revision(added))
|
|
670
|
+
assert(result[0] === 'ok', ['expected revision ok', result])
|
|
671
|
+
assertEq(result[1].lock, canonical)
|
|
672
|
+
const [, readded] = virtualOk(state2)(e.add(result[1]))
|
|
673
|
+
assertEq(readded, added)
|
|
674
|
+
},
|
|
675
|
+
// A shared-lock reference is checked as a hash on the way in, so a
|
|
676
|
+
// non-cBase32 one fails the write instead of being stored unresolvable.
|
|
677
|
+
revisionInvalidSharedLockReferenceIsRejected: () => {
|
|
678
|
+
const c = fileCas(sha256)(home)
|
|
679
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
680
|
+
const e = evo(c)(cacheKey)
|
|
681
|
+
const [, result] = virtual(state0)(e.add({
|
|
682
|
+
parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0xffn)),
|
|
683
|
+
lock: 'https://example.com/lock',
|
|
684
|
+
}))
|
|
685
|
+
assertEvoError(result, 'lock reference is not a valid hash')
|
|
686
|
+
},
|
|
687
|
+
// A nested lock map round-trips with its scope structure intact, and
|
|
688
|
+
// canonicalization recurses into it: an alias spelling bound inside a
|
|
689
|
+
// scope comes back canonical, like a direct one.
|
|
690
|
+
revisionNestedLockRoundTripsAndCanonicalizes: () => {
|
|
691
|
+
const c = fileCas(sha256)(home)
|
|
692
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
693
|
+
const e = evo(c)(cacheKey)
|
|
694
|
+
const canonical = vecToCBase32(vec8(0xffn))
|
|
695
|
+
const alias = canonical.toUpperCase()
|
|
696
|
+
const [state1, added] = virtualOk(state0)(e.add({
|
|
697
|
+
parents: [], subject: 'doc', snapshot: canonical,
|
|
698
|
+
lock: { B: { D: alias }, C: canonical },
|
|
699
|
+
}))
|
|
700
|
+
const [state2, result] = virtual(state1)(e.revision(added))
|
|
655
701
|
assert(result[0] === 'ok', ['expected revision ok', result])
|
|
656
|
-
|
|
702
|
+
const lock = result[1].lock
|
|
703
|
+
assert(typeof lock === 'object', ['expected an inline lock map', lock])
|
|
704
|
+
const scope = lock.B
|
|
705
|
+
assert(typeof scope === 'object', ['expected a nested scope', scope])
|
|
706
|
+
assertEq(scope.D, canonical)
|
|
707
|
+
// The read value is still valid `add` input, and re-adding it reuses
|
|
708
|
+
// the same address — the round trip loses nothing.
|
|
709
|
+
const [, readded] = virtualOk(state2)(e.add(result[1]))
|
|
710
|
+
assertEq(readded, added)
|
|
711
|
+
},
|
|
712
|
+
// Semantic checking recurses: a non-hash string inside a nested scope
|
|
713
|
+
// fails the write, like one at the root.
|
|
714
|
+
revisionInvalidNestedLockValueIsRejected: () => {
|
|
715
|
+
const c = fileCas(sha256)(home)
|
|
716
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
717
|
+
const e = evo(c)(cacheKey)
|
|
718
|
+
const [, result] = virtual(state0)(e.add({
|
|
719
|
+
parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x42n)),
|
|
720
|
+
lock: { B: { dependency: 'not a hash!' } },
|
|
721
|
+
}))
|
|
722
|
+
assertEvoError(result, 'B/dependency')
|
|
657
723
|
},
|
|
658
724
|
revisionAbsentAndEmptyLocksRemainDistinct: () => {
|
|
659
725
|
const c = fileCas(sha256)(home)
|
|
660
|
-
const [state0, cacheKey] =
|
|
726
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
661
727
|
const e = evo(c)(cacheKey)
|
|
662
728
|
const snapshot = vecToCBase32(vec8(0x42n))
|
|
663
|
-
const [state1, absent] =
|
|
664
|
-
|
|
665
|
-
const [
|
|
666
|
-
assert(empty[0] === 'ok', ['expected add ok', empty])
|
|
667
|
-
const [state3, absentRead] = virtual(state2)(e.revision(absent[1]))
|
|
729
|
+
const [state1, absent] = virtualOk(state0)(e.add({ parents: [], subject: 'absent', snapshot }))
|
|
730
|
+
const [state2, empty] = virtualOk(state1)(e.add({ parents: [], subject: 'empty', snapshot, lock: {} }))
|
|
731
|
+
const [state3, absentRead] = virtual(state2)(e.revision(absent))
|
|
668
732
|
assert(absentRead[0] === 'ok', ['expected revision ok', absentRead])
|
|
669
|
-
const [, emptyRead] = virtual(state3)(e.revision(empty
|
|
733
|
+
const [, emptyRead] = virtual(state3)(e.revision(empty))
|
|
670
734
|
assert(emptyRead[0] === 'ok', ['expected revision ok', emptyRead])
|
|
671
735
|
assertEq(absentRead[1].lock, undefined)
|
|
672
736
|
assertEq(Object.keys(emptyRead[1].lock ?? {}).length, 0)
|
|
673
737
|
},
|
|
674
738
|
revisionInvalidLockValueIsRejected: () => {
|
|
675
739
|
const c = fileCas(sha256)(home)
|
|
676
|
-
const [state0, cacheKey] =
|
|
740
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
677
741
|
const e = evo(c)(cacheKey)
|
|
678
742
|
const [, result] = virtual(state0)(e.add({
|
|
679
743
|
parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x42n)),
|
|
680
744
|
lock: { dependency: 'not a hash!' },
|
|
681
745
|
}))
|
|
682
|
-
|
|
746
|
+
assertEvoError(result, 'dependency')
|
|
683
747
|
},
|
|
684
748
|
equivalentLockOrdersReuseOneCasAddress: () => {
|
|
685
749
|
const c = fileCas(sha256)(home)
|
|
686
|
-
const [state0, cacheKey] =
|
|
750
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
751
|
+
const e = evo(c)(cacheKey)
|
|
752
|
+
const a = vecToCBase32(vec8(0x1an))
|
|
753
|
+
const b = vecToCBase32(vec8(0x2bn))
|
|
754
|
+
const snapshot = vecToCBase32(vec8(0x3cn))
|
|
755
|
+
const [state1, first] = virtualOk(state0)(e.add({ parents: [], subject: 'doc', snapshot, lock: { '2': b, '10': a } }))
|
|
756
|
+
const [, second] = virtualOk(state1)(e.add({ lock: { '10': a, '2': b }, snapshot, subject: 'doc', parents: [] }))
|
|
757
|
+
assertEq(second, first)
|
|
758
|
+
},
|
|
759
|
+
// Canonical serialization reaches into nested scopes, so two adds
|
|
760
|
+
// differing only in the property order of a nested map — at the root and
|
|
761
|
+
// inside the scope alike — deduplicate to one blob rather than leaving
|
|
762
|
+
// two heads.
|
|
763
|
+
equivalentNestedLockOrdersReuseOneCasAddress: () => {
|
|
764
|
+
const c = fileCas(sha256)(home)
|
|
765
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
687
766
|
const e = evo(c)(cacheKey)
|
|
688
767
|
const a = vecToCBase32(vec8(0x1an))
|
|
689
768
|
const b = vecToCBase32(vec8(0x2bn))
|
|
690
769
|
const snapshot = vecToCBase32(vec8(0x3cn))
|
|
691
|
-
const [state1, first] =
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
770
|
+
const [state1, first] = virtualOk(state0)(e.add({
|
|
771
|
+
parents: [], subject: 'doc', snapshot,
|
|
772
|
+
lock: { B: { '2': b, '10': a }, A: a },
|
|
773
|
+
}))
|
|
774
|
+
const [, second] = virtualOk(state1)(e.add({
|
|
775
|
+
parents: [], subject: 'doc', snapshot,
|
|
776
|
+
lock: { A: a, B: { '10': a, '2': b } },
|
|
777
|
+
}))
|
|
778
|
+
assertEq(second, first)
|
|
696
779
|
},
|
|
697
780
|
// A raw CAS write (e.g. `cas_add`) of valid revision content is folded
|
|
698
781
|
// into the cache exactly as `addRevision` would, without going through
|
|
@@ -700,29 +783,29 @@ export const proof = {
|
|
|
700
783
|
// same store consistent (see `fjs/mcp`).
|
|
701
784
|
syncRevisionFoldsValidRevisionIntoCache: () => {
|
|
702
785
|
const c = fileCas(sha256)(home)
|
|
703
|
-
const [state0, cacheKey] =
|
|
786
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
704
787
|
const subjectHash = vecToCBase32(vec8(0x13n))
|
|
705
788
|
const text = `{"dialect":"${revisionDialect}","subject":"${subjectHash}","parents":[],"snapshot":"${subjectHash}","generation":0}`
|
|
706
789
|
const bytes = tryUtf8(text)
|
|
707
790
|
assert(bytes !== null, 'expected sample revision to encode as UTF-8')
|
|
708
791
|
const hashVec = vec8(0x14n)
|
|
709
|
-
const [state1] =
|
|
710
|
-
const [, heads] =
|
|
792
|
+
const [state1] = virtualOk(state0)(syncRevision(cacheKey)(hashVec)(bytes))
|
|
793
|
+
const [, heads] = virtualOk(state1)(evo(c)(cacheKey).head(subjectHash))
|
|
711
794
|
assertEq(heads.length, 1)
|
|
712
795
|
assertEq(heads[0], vecToCBase32(hashVec))
|
|
713
796
|
},
|
|
714
797
|
syncRevisionIgnoresNonRevisionContent: () => {
|
|
715
798
|
const c = fileCas(sha256)(home)
|
|
716
|
-
const [state0, cacheKey] =
|
|
717
|
-
const [state1] =
|
|
718
|
-
const [, subjects] =
|
|
799
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
800
|
+
const [state1] = virtualOk(state0)(syncRevision(cacheKey)(vec8(0x15n))(vec8(0x41n)))
|
|
801
|
+
const [, subjects] = virtualOk(state1)(evo(c)(cacheKey).list())
|
|
719
802
|
assertEq(subjects.length, 0)
|
|
720
803
|
},
|
|
721
804
|
evoHeadUnknownSubjectIsEmpty: () => {
|
|
722
805
|
const c = fileCas(sha256)(home)
|
|
723
|
-
const [state0, cacheKey] =
|
|
806
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
724
807
|
const e = evo(c)(cacheKey)
|
|
725
|
-
const [, heads] =
|
|
808
|
+
const [, heads] = virtualOk(state0)(e.head('nope'))
|
|
726
809
|
assertEq(heads.length, 0)
|
|
727
810
|
},
|
|
728
811
|
// Regression: subjects are arbitrary caller-supplied strings, so one can
|
|
@@ -732,21 +815,20 @@ export const proof = {
|
|
|
732
815
|
// `.hashes` — the cache must use an own-property lookup instead.
|
|
733
816
|
evoHeadUnknownPrototypeNamedSubjectIsEmpty: () => {
|
|
734
817
|
const c = fileCas(sha256)(home)
|
|
735
|
-
const [state0, cacheKey] =
|
|
818
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
736
819
|
const e = evo(c)(cacheKey)
|
|
737
|
-
const [, heads] =
|
|
820
|
+
const [, heads] = virtualOk(state0)(e.head('constructor'))
|
|
738
821
|
assertEq(heads.length, 0)
|
|
739
822
|
},
|
|
740
823
|
evoAddAndHeadHandlePrototypeNamedSubject: () => {
|
|
741
824
|
const c = fileCas(sha256)(home)
|
|
742
|
-
const [state0, cacheKey] =
|
|
825
|
+
const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
|
|
743
826
|
const e = evo(c)(cacheKey)
|
|
744
827
|
/** @type {RevisionData} */
|
|
745
828
|
const input = { parents: [], subject: 'toString', snapshot: vecToCBase32(vec8(0x16n)) }
|
|
746
|
-
const [state1, result] =
|
|
747
|
-
|
|
748
|
-
const [, heads] = virtual(state1)(e.head('toString'))
|
|
829
|
+
const [state1, result] = virtualOk(state0)(e.add(input))
|
|
830
|
+
const [, heads] = virtualOk(state1)(e.head('toString'))
|
|
749
831
|
assertEq(heads.length, 1)
|
|
750
|
-
assertEq(heads[0], result
|
|
832
|
+
assertEq(heads[0], result)
|
|
751
833
|
},
|
|
752
834
|
}
|