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/proof.f.mjs
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @import { Vec } from '../types/bit_vec/types.ts'
|
|
3
3
|
* @import { FileCasOperation } from './types.ts'
|
|
4
|
+
* @import { IoChannel, IoResult, Mkdir, ReadFile, WriteFile } from '../effects/node/types.ts'
|
|
4
5
|
* @import { Effect } from '../effects/types.ts'
|
|
5
|
-
* @import {
|
|
6
|
-
* @import { Ok } from '../types/result/types.ts'
|
|
6
|
+
* @import { Ok, Result } from '../types/result/types.ts'
|
|
7
7
|
* @import { List } from '../effects/list/types.ts'
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { length, maxLength, msb, vec, vec8 } from '../types/bit_vec/module.f.mjs'
|
|
11
11
|
import { cBase32ToVec, vecToCBase32 } from '../basen/cbase32/module.f.mjs'
|
|
12
12
|
import { computeSync, sha256 } from '../crypto/sha2/module.f.mjs'
|
|
13
|
-
import { fileCas, casAddFile, collectRead
|
|
14
|
-
import { match,
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
13
|
+
import { fileCas, casAddFile, collectRead } from './module.f.mjs'
|
|
14
|
+
import { match, runPure } from '../effects/module.f.mjs'
|
|
15
|
+
import { mapStep as ioMapStep, pureError, pureOk, step as ioStep } from '../effects/module.f.mjs'
|
|
16
|
+
import { ioError, mkdir, writeFile, readFile, access } from '../effects/node/module.f.mjs'
|
|
17
|
+
import { error, ok, unwrap as unwrapResult } from '../types/result/module.f.mjs'
|
|
17
18
|
import { emptyState, virtual } from '../effects/node/virtual/module.f.mjs'
|
|
18
19
|
import { join } from '../path/module.f.mjs'
|
|
19
20
|
import { nonEmpty, empty } from '../effects/list/module.f.mjs'
|
|
@@ -21,7 +22,7 @@ import { assert, assertEq, assertNotNullish } from '../asserts/module.f.mjs'
|
|
|
21
22
|
|
|
22
23
|
const testDir = './test-cas-cli'
|
|
23
24
|
|
|
24
|
-
/** @typedef {FileCasOperation | WriteFile | ReadFile |
|
|
25
|
+
/** @typedef {FileCasOperation | WriteFile | ReadFile | Mkdir} _TestOp */
|
|
25
26
|
|
|
26
27
|
// Names the command a `FileCasOperation` effect stops at, so a proof can assert
|
|
27
28
|
// on it and resume the continuation without reading the `Do` layout. The map
|
|
@@ -41,8 +42,98 @@ const casCommand = match({
|
|
|
41
42
|
writeBytes: () => 'writeBytes',
|
|
42
43
|
})
|
|
43
44
|
|
|
45
|
+
// A harmless "always succeeds" response for a command, used by `drive` once a
|
|
46
|
+
// test's overrides for that command are exhausted — good enough to let the
|
|
47
|
+
// rest of `write`'s pipeline run to completion without ever touching a real
|
|
48
|
+
// filesystem.
|
|
49
|
+
/** @type {(cmd: string) => unknown} */
|
|
50
|
+
const casDefaultResponse = cmd => {
|
|
51
|
+
switch (cmd) {
|
|
52
|
+
case 'now': return ok(0)
|
|
53
|
+
case 'randomInt': return ok(0)
|
|
54
|
+
case 'mkdir': case 'createExclusive': case 'rename': case 'rm':
|
|
55
|
+
case 'writeBytes': case 'access':
|
|
56
|
+
return ok(undefined)
|
|
57
|
+
case 'readdir': return ok([])
|
|
58
|
+
case 'stat': return ok({ size: 0 })
|
|
59
|
+
default: return ok(undefined)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Drives a `FileCasOperation` effect to completion with synthetic op
|
|
65
|
+
* responses instead of a filesystem. `overrides[cmd]` is a queue consumed in
|
|
66
|
+
* call order; once a command's queue is empty (or was never given),
|
|
67
|
+
* `casDefaultResponse` supplies an always-succeeds value.
|
|
68
|
+
*
|
|
69
|
+
* `write`'s op-failure branches — a `writeBytes`/`rename` call failing
|
|
70
|
+
* mid-stream, or the final `stat` reporting a mismatched size — are real only
|
|
71
|
+
* under a race (another writer, a failing disk) between two of `write`'s own
|
|
72
|
+
* steps. The virtual harness has no such race to offer, so this reaches them
|
|
73
|
+
* directly instead: `overrides` names exactly the one call that fails, and
|
|
74
|
+
* every other call in the pipeline succeeds around it.
|
|
75
|
+
*
|
|
76
|
+
* Also returns the ordered log of command names issued, so a caller can
|
|
77
|
+
* confirm not just the returned error but *that cleanup ran* — e.g. that the
|
|
78
|
+
* failure path's own `rm` of the partial staging file was actually called,
|
|
79
|
+
* not just that some code path returned the right error tag.
|
|
80
|
+
*
|
|
81
|
+
* @type {(overrides: Partial<Record<string, unknown[]>>) => (e: Effect<FileCasOperation, unknown, unknown>) => readonly [Result<unknown, unknown>, readonly string[]]}
|
|
82
|
+
*/
|
|
83
|
+
const drive = overrides => {
|
|
84
|
+
/** @type {string[]} */
|
|
85
|
+
const log = []
|
|
86
|
+
/** @type {(cmd: string) => unknown} */
|
|
87
|
+
const next = cmd => {
|
|
88
|
+
log.push(cmd)
|
|
89
|
+
const queue = overrides[cmd]
|
|
90
|
+
return queue !== undefined && queue.length > 0 ? queue.shift() : casDefaultResponse(cmd)
|
|
91
|
+
}
|
|
92
|
+
const handlers = {
|
|
93
|
+
access: () => next('access'),
|
|
94
|
+
createExclusive: () => next('createExclusive'),
|
|
95
|
+
mkdir: () => next('mkdir'),
|
|
96
|
+
now: () => next('now'),
|
|
97
|
+
randomInt: () => next('randomInt'),
|
|
98
|
+
readBytes: () => next('readBytes'),
|
|
99
|
+
readdir: () => next('readdir'),
|
|
100
|
+
rename: () => next('rename'),
|
|
101
|
+
rm: () => next('rm'),
|
|
102
|
+
stat: () => next('stat'),
|
|
103
|
+
writeBytes: () => next('writeBytes'),
|
|
104
|
+
}
|
|
105
|
+
const matcher = match(handlers)
|
|
106
|
+
/** @type {(e: Effect<FileCasOperation, unknown, unknown>) => Result<unknown, unknown>} */
|
|
107
|
+
const run_ = e => {
|
|
108
|
+
const m = matcher(e)
|
|
109
|
+
return m[0] === 'done' ? m[1] : run_(m[2](m[1]))
|
|
110
|
+
}
|
|
111
|
+
return e => [run_(e), log]
|
|
112
|
+
}
|
|
113
|
+
|
|
44
114
|
// Create a 128 KiB big file content (at the max Vec size limit)
|
|
45
115
|
// This tests the boundary where files are at the chunk size limit
|
|
116
|
+
/**
|
|
117
|
+
* Asserts that a channel error is a host failure carrying `message`.
|
|
118
|
+
* @type {(e: unknown, message: string) => void}
|
|
119
|
+
*/
|
|
120
|
+
const assertIoMessage = (e, message) => {
|
|
121
|
+
assert(e instanceof Array && e[0] === 'ioError', ['expected an ioError', e])
|
|
122
|
+
assertEq(e[1].message, message)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The error a `drive`d effect answered with. The tuple shape is the driver's
|
|
127
|
+
* declared result type now, so only the tag is checked — and it is still
|
|
128
|
+
* checked, because these proofs exist to establish that a write fails closed.
|
|
129
|
+
*
|
|
130
|
+
* @type {(result: Result<unknown, unknown>) => unknown}
|
|
131
|
+
*/
|
|
132
|
+
const errorMessage = result => {
|
|
133
|
+
assert(result[0] === 'error', ['expected an error result', result])
|
|
134
|
+
return result[1]
|
|
135
|
+
}
|
|
136
|
+
|
|
46
137
|
/** @type {() => Vec} */
|
|
47
138
|
const createBigFileContent = () => {
|
|
48
139
|
const byteCount = 128n * 1024n // 128 KiB
|
|
@@ -50,107 +141,89 @@ const createBigFileContent = () => {
|
|
|
50
141
|
return vec(byteCount * 8n)(0x42424242n)
|
|
51
142
|
}
|
|
52
143
|
|
|
53
|
-
// Test adding a big file and verifying the hash
|
|
54
|
-
|
|
144
|
+
// Test adding a big file and verifying the hash.
|
|
145
|
+
//
|
|
146
|
+
// Each link is an `ioStep`, so a failure short-circuits the rest instead of
|
|
147
|
+
// being asserted away link by link: the continuations below see values, and
|
|
148
|
+
// the proof checks the one result at the end.
|
|
149
|
+
//
|
|
150
|
+
// Neither this chain nor the next one ends in cleanup any more. Both used to
|
|
151
|
+
// finish with an `rm(testDir)` whose result the raw `step` then discarded —
|
|
152
|
+
// and the virtual filesystem cannot remove a *non-empty* directory, so that
|
|
153
|
+
// `rm` had been failing on every run without anything noticing. There is also
|
|
154
|
+
// nothing to clean: each run interprets against a fresh `emptyState`.
|
|
155
|
+
/** @type {() => Effect<_TestOp, void, IoChannel>} */
|
|
55
156
|
const testAddBigFile = () => {
|
|
56
157
|
const bigFilePath = `${testDir}/big-file.bin`
|
|
57
158
|
const cas = fileCas(sha256)(testDir)
|
|
58
|
-
const x0 =
|
|
159
|
+
const x0 = ioStep(
|
|
59
160
|
mkdir(testDir, { recursive: true }),
|
|
60
161
|
() => writeFile(bigFilePath, createBigFileContent())
|
|
61
162
|
)
|
|
62
|
-
const x1 =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
const x2 = step(
|
|
70
|
-
x1,
|
|
71
|
-
addRes => {
|
|
72
|
-
assert(addRes[0] === 'ok', ['failed to add file to CAS', addRes])
|
|
73
|
-
const hash = addRes[1]
|
|
74
|
-
// Verify hash is 256 bits (SHA-256)
|
|
75
|
-
assertEq(length(hash), 256n, ['expected 256-bit hash', length(hash)])
|
|
76
|
-
// Verify hash can be encoded/decoded
|
|
77
|
-
assertNotNullish(cBase32ToVec(vecToCBase32(hash)), 'failed to decode hash from cBase32')
|
|
78
|
-
return rm(testDir)
|
|
79
|
-
})
|
|
80
|
-
return step(
|
|
81
|
-
x2,
|
|
82
|
-
() => pure(undefined)
|
|
83
|
-
)
|
|
163
|
+
const x1 = ioStep(x0, () => casAddFile(cas)(bigFilePath))
|
|
164
|
+
return ioMapStep(x1, hash => {
|
|
165
|
+
// Verify hash is 256 bits (SHA-256)
|
|
166
|
+
assertEq(length(hash), 256n, ['expected 256-bit hash', length(hash)])
|
|
167
|
+
// Verify hash can be encoded/decoded
|
|
168
|
+
assertNotNullish(cBase32ToVec(vecToCBase32(hash)), 'failed to decode hash from cBase32')
|
|
169
|
+
})
|
|
84
170
|
}
|
|
85
171
|
|
|
86
172
|
// Test adding and retrieving a big file
|
|
87
|
-
/** @type {() => Effect<_TestOp, void>} */
|
|
173
|
+
/** @type {() => Effect<_TestOp, void, IoChannel>} */
|
|
88
174
|
const testAddAndGetBigFile = () => {
|
|
89
175
|
const bigContent = createBigFileContent()
|
|
90
176
|
const bigFilePath = `${testDir}/big-file.bin`
|
|
91
177
|
const cas = fileCas(sha256)(testDir)
|
|
92
|
-
const x0 =
|
|
178
|
+
const x0 = ioStep(
|
|
93
179
|
mkdir(testDir, { recursive: true }),
|
|
94
180
|
() => writeFile(bigFilePath, bigContent)
|
|
95
181
|
)
|
|
96
|
-
const x1 =
|
|
97
|
-
x0,
|
|
98
|
-
writeRes => {
|
|
99
|
-
assert(writeRes[0] === 'ok', ['failed to write test file', writeRes])
|
|
100
|
-
return casAddFile(cas)(bigFilePath)
|
|
101
|
-
}
|
|
102
|
-
)
|
|
182
|
+
const x1 = ioStep(x0, () => casAddFile(cas)(bigFilePath))
|
|
103
183
|
// Verify the file is stored at the expected location
|
|
104
|
-
const x2 =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
)
|
|
111
|
-
const x3 = step(
|
|
112
|
-
x2,
|
|
113
|
-
readRes => {
|
|
114
|
-
assert(readRes[0] === 'ok', ['failed to read stored file', readRes])
|
|
115
|
-
// Verify content is the same size as original
|
|
116
|
-
assertEq(length(readRes[1]), length(bigContent), 'stored content size mismatch')
|
|
117
|
-
return rm(testDir)
|
|
118
|
-
})
|
|
119
|
-
return step(
|
|
120
|
-
x3,
|
|
121
|
-
() => pure(undefined)
|
|
122
|
-
)
|
|
184
|
+
const x2 = ioStep(x1, hash => readFile(cas.url(hash)))
|
|
185
|
+
return ioMapStep(x2, stored => {
|
|
186
|
+
// Verify content is the same size as original
|
|
187
|
+
assertEq(length(stored), length(bigContent), 'stored content size mismatch')
|
|
188
|
+
})
|
|
123
189
|
}
|
|
124
190
|
|
|
125
191
|
export const proof = {
|
|
126
|
-
// Both effects must be interpreted, not merely built: an
|
|
192
|
+
// Both effects must be interpreted, not merely built: an effect is inert
|
|
127
193
|
// data, so returning one from a proof runs none of its continuations and
|
|
128
|
-
// asserts nothing.
|
|
129
|
-
|
|
130
|
-
|
|
194
|
+
// asserts nothing. The result is checked too — an error short-circuits the
|
|
195
|
+
// chain, so a run that never reached the asserts inside it would otherwise
|
|
196
|
+
// pass silently.
|
|
197
|
+
addBigFile: () => {
|
|
198
|
+
const [, r] = virtual(emptyState)(testAddBigFile())
|
|
199
|
+
assert(r[0] === 'ok', ['expected the big-file round trip to succeed', r])
|
|
200
|
+
},
|
|
201
|
+
addAndGetBigFile: () => {
|
|
202
|
+
const [, r] = virtual(emptyState)(testAddAndGetBigFile())
|
|
203
|
+
assert(r[0] === 'ok', ['expected the big-file read-back to succeed', r])
|
|
204
|
+
},
|
|
131
205
|
//
|
|
132
206
|
casWriteRead: () => {
|
|
133
207
|
// Round-trip a single-chunk payload through the real streaming CAS: `write` returns
|
|
134
208
|
// the content hash, and `read` streams the same bytes back as `ok` chunk items.
|
|
135
209
|
const content = vec8(0x2An)
|
|
136
210
|
const c = fileCas(sha256)('.')
|
|
137
|
-
/** @type {List<FileCasOperation,
|
|
138
|
-
const payload = nonEmpty(
|
|
211
|
+
/** @type {List<FileCasOperation, Vec, IoChannel>} */
|
|
212
|
+
const payload = nonEmpty(content, empty())
|
|
139
213
|
const [state1, writeResult] = virtual(emptyState)(c.write(payload))
|
|
140
214
|
assert(writeResult[0] === 'ok', ['expected write ok', writeResult])
|
|
141
215
|
const hash = writeResult[1]
|
|
142
216
|
assertEq(length(hash), 256n, ['expected 256-bit hash', length(hash)])
|
|
143
217
|
assertEq(msb.cmp(hash)(computeSync(sha256)([content])), 0, 'write hash mismatch')
|
|
144
|
-
/** @type {(acc: readonly Vec[]) => (stream: List<FileCasOperation,
|
|
218
|
+
/** @type {(acc: readonly Vec[]) => (stream: List<FileCasOperation, Vec, IoChannel>) => Effect<FileCasOperation, readonly Vec[], IoChannel>} */
|
|
145
219
|
const drain = acc =>
|
|
146
220
|
stream =>
|
|
147
|
-
|
|
221
|
+
ioStep(
|
|
148
222
|
stream,
|
|
149
223
|
(node) => {
|
|
150
|
-
if (node === undefined) { return
|
|
224
|
+
if (node === undefined) { return pureOk(acc) }
|
|
151
225
|
const { first, tail } = node
|
|
152
|
-
|
|
153
|
-
return drain([...acc, first[1]])(tail)
|
|
226
|
+
return drain([...acc, first])(tail)
|
|
154
227
|
},
|
|
155
228
|
)
|
|
156
229
|
const [, readResult] = virtual(state1)(drain([])(c.read(hash)))
|
|
@@ -158,12 +231,12 @@ export const proof = {
|
|
|
158
231
|
assertEq(msb.cmp(msb.listToVec(readResult[1]))(content), 0, 'read content mismatch')
|
|
159
232
|
},
|
|
160
233
|
casReadMissingShard: () => {
|
|
161
|
-
// A missing shard
|
|
234
|
+
// A missing shard fails the stream; it is never the `ok(undefined)` that
|
|
235
|
+
// ends one, which is the confusion a per-item `Result` used to allow.
|
|
162
236
|
const c = fileCas(sha256)('.')
|
|
163
237
|
const hash = computeSync(sha256)([vec8(0x2An)])
|
|
164
|
-
const
|
|
165
|
-
assert(
|
|
166
|
-
assert(node.first[0] === 'error', ['expected error item', node.tail])
|
|
238
|
+
const cell = virtual(emptyState)(c.read(hash))[1]
|
|
239
|
+
assert(cell[0] === 'error', ['missing shard must fail the stream', cell])
|
|
167
240
|
},
|
|
168
241
|
casWriteMultiChunk: () => {
|
|
169
242
|
// A multi-chunk payload streams through `writeBytes` chunk-by-chunk (the lease is
|
|
@@ -171,24 +244,23 @@ export const proof = {
|
|
|
171
244
|
// and read streams the same content back.
|
|
172
245
|
const chunks = /** @type {const} */ ([vec8(0x11n), vec8(0x22n), vec8(0x33n)])
|
|
173
246
|
const c = fileCas(sha256)('.')
|
|
174
|
-
/** @type {List<FileCasOperation,
|
|
247
|
+
/** @type {List<FileCasOperation, Vec, IoChannel>} */
|
|
175
248
|
const payload = chunks.reduceRight(
|
|
176
|
-
(tail, chunk) => nonEmpty(
|
|
177
|
-
/** @
|
|
249
|
+
(tail, chunk) => nonEmpty(chunk, tail),
|
|
250
|
+
/** @satisfies {List<never, Vec, IoChannel>} */ (empty()))
|
|
178
251
|
const [state1, writeResult] = virtual(emptyState)(c.write(payload))
|
|
179
252
|
assert(writeResult[0] === 'ok', ['expected write ok', writeResult])
|
|
180
253
|
const hash = writeResult[1]
|
|
181
254
|
assertEq(msb.cmp(hash)(computeSync(sha256)(chunks)), 0, 'multi-chunk write hash mismatch')
|
|
182
|
-
/** @type {(acc: readonly Vec[]) => (stream: List<FileCasOperation,
|
|
255
|
+
/** @type {(acc: readonly Vec[]) => (stream: List<FileCasOperation, Vec, IoChannel>) => Effect<FileCasOperation, readonly Vec[], IoChannel>} */
|
|
183
256
|
const drain = acc =>
|
|
184
257
|
stream =>
|
|
185
|
-
|
|
258
|
+
ioStep(
|
|
186
259
|
stream,
|
|
187
260
|
(node) => {
|
|
188
|
-
if (node === undefined) { return
|
|
261
|
+
if (node === undefined) { return pureOk(acc) }
|
|
189
262
|
const { first, tail } = node
|
|
190
|
-
|
|
191
|
-
return drain([...acc, first[1]])(tail)
|
|
263
|
+
return drain([...acc, first])(tail)
|
|
192
264
|
},
|
|
193
265
|
)
|
|
194
266
|
const [, readResult] = virtual(state1)(drain([])(c.read(hash)))
|
|
@@ -201,28 +273,26 @@ export const proof = {
|
|
|
201
273
|
// first, leaving exactly one shard in the store.
|
|
202
274
|
const content = vec8(0x2An)
|
|
203
275
|
const c = fileCas(sha256)('.')
|
|
204
|
-
/** @type {() => List<FileCasOperation,
|
|
205
|
-
const payload = () => nonEmpty(
|
|
276
|
+
/** @type {() => List<FileCasOperation, Vec, IoChannel>} */
|
|
277
|
+
const payload = () => nonEmpty(content, empty())
|
|
206
278
|
const [state1, w1] = virtual(emptyState)(c.write(payload()))
|
|
207
279
|
const [state2, w2] = virtual(state1)(c.write(payload()))
|
|
208
280
|
assert(!(w1[0] !== 'ok' || w2[0] !== 'ok'), ['expected both writes ok', w1, w2])
|
|
209
281
|
assertEq(msb.cmp(w1[1])(w2[1]), 0, 'dedup hash mismatch')
|
|
210
|
-
const [,
|
|
282
|
+
const [, listed] = virtual(state2)(c.list())
|
|
283
|
+
const hashes = unwrapResult(listed)
|
|
211
284
|
assertEq(hashes.length, 1, ['expected one shard after dedup', hashes.length])
|
|
212
285
|
},
|
|
213
|
-
|
|
214
|
-
//
|
|
286
|
+
casWriteStreamFailureAborts: () => {
|
|
287
|
+
// A stream that fails mid-way deletes the partial staging file and fails; nothing is
|
|
215
288
|
// published, so the store stays empty.
|
|
216
289
|
const c = fileCas(sha256)('.')
|
|
217
|
-
/** @type {
|
|
218
|
-
const
|
|
219
|
-
/** @type {IoResult<Vec>} */
|
|
220
|
-
const errItem = error({ code: 'BOOM' })
|
|
221
|
-
/** @type {List<FileCasOperation, IoResult<Vec>>} */
|
|
222
|
-
const payload = nonEmpty(okItem, nonEmpty(errItem, /** @type {List<FileCasOperation, IoResult<Vec>>} */ (empty())))
|
|
290
|
+
/** @type {List<FileCasOperation, Vec, IoChannel>} */
|
|
291
|
+
const payload = nonEmpty(vec8(0x11n), pureError(ioError({ code: 'BOOM', message: 'boom' })))
|
|
223
292
|
const [state1, result] = virtual(emptyState)(c.write(payload))
|
|
224
293
|
assert(result[0] === 'error', ['expected write error', result])
|
|
225
|
-
const [,
|
|
294
|
+
const [, listed] = virtual(state1)(c.list())
|
|
295
|
+
const hashes = unwrapResult(listed)
|
|
226
296
|
assertEq(hashes.length, 0, ['expected nothing published on abort', hashes])
|
|
227
297
|
},
|
|
228
298
|
casWriteReadExceedsMaxLength: () => {
|
|
@@ -235,25 +305,24 @@ export const proof = {
|
|
|
235
305
|
const tail = vec8(0x2An) // one more byte ⇒ total > maxLength
|
|
236
306
|
const chunks = /** @type {const} */ ([big, tail])
|
|
237
307
|
const c = fileCas(sha256)('.')
|
|
238
|
-
/** @type {List<FileCasOperation,
|
|
308
|
+
/** @type {List<FileCasOperation, Vec, IoChannel>} */
|
|
239
309
|
const payload = chunks.reduceRight(
|
|
240
|
-
(tl, chunk) => nonEmpty(
|
|
241
|
-
/** @
|
|
310
|
+
(tl, chunk) => nonEmpty(chunk, tl),
|
|
311
|
+
/** @satisfies {List<never, Vec, IoChannel>} */ (empty()))
|
|
242
312
|
const [state1, w] = virtual(emptyState)(c.write(payload))
|
|
243
313
|
assert(w[0] === 'ok', ['expected write ok', w])
|
|
244
314
|
const hash = w[1]
|
|
245
315
|
assertEq(msb.cmp(hash)(computeSync(sha256)(chunks)), 0, 'oversized write hash mismatch')
|
|
246
316
|
// Fold the read stream straight into a fresh SHA-2 state — never one `Vec`.
|
|
247
|
-
/** @type {(state: typeof sha256.init) => (stream: List<FileCasOperation,
|
|
317
|
+
/** @type {(state: typeof sha256.init) => (stream: List<FileCasOperation, Vec, IoChannel>) => Effect<FileCasOperation, Vec, IoChannel>} */
|
|
248
318
|
const rehash = state =>
|
|
249
319
|
stream =>
|
|
250
|
-
|
|
320
|
+
ioStep(
|
|
251
321
|
stream,
|
|
252
322
|
(node) => {
|
|
253
|
-
if (node === undefined) { return
|
|
323
|
+
if (node === undefined) { return pureOk(sha256.end(state)) }
|
|
254
324
|
const { first, tail } = node
|
|
255
|
-
|
|
256
|
-
return rehash(sha256.append(first[1])(state))(tail)
|
|
325
|
+
return rehash(sha256.append(first)(state))(tail)
|
|
257
326
|
}
|
|
258
327
|
)
|
|
259
328
|
const [, readBack] = virtual(state1)(rehash(sha256.init)(c.read(hash)))
|
|
@@ -271,12 +340,32 @@ export const proof = {
|
|
|
271
340
|
}
|
|
272
341
|
const content = vec8(0x2An)
|
|
273
342
|
const c = fileCas(sha256)('.')
|
|
274
|
-
const x = c.write(nonEmpty(
|
|
343
|
+
const x = c.write(nonEmpty(content, /** @satisfies {List<never, Vec, IoChannel>} */ (empty())))
|
|
275
344
|
const [state1, w] = virtual(state0)(x)
|
|
276
345
|
assert(w[0] === 'ok', ['expected write ok', w])
|
|
277
346
|
const [, present] = virtual(state1)(access(stalePath))
|
|
278
347
|
assert(present[0] === 'error', 'expected GC to reclaim the expired staging file')
|
|
279
348
|
},
|
|
349
|
+
casWriteGcRmFailureDoesNotFailTheUpload: () => {
|
|
350
|
+
// The sweep is best-effort, and `gcStage`'s error channel is `never`
|
|
351
|
+
// because it says so: an `rm` that fails on one expired file recovers
|
|
352
|
+
// per item, so the upload it is piggy-backed on still succeeds.
|
|
353
|
+
const stale = '0000000000000000000-stale'
|
|
354
|
+
const c = fileCas(sha256)('.')
|
|
355
|
+
// An empty payload: the driver's default `stat` reports size 0, so the
|
|
356
|
+
// publish check passes and the sweep is the only thing under test.
|
|
357
|
+
/** @type {List<never, Vec, IoChannel>} */
|
|
358
|
+
const payload = empty()
|
|
359
|
+
const [result, log] = drive({
|
|
360
|
+
// Only the first `now` is the sweep's, so every deadline after it
|
|
361
|
+
// stays 0 and the staging file below reads as expired.
|
|
362
|
+
now: [ok(1)],
|
|
363
|
+
readdir: [ok([{ name: stale, parentPath: '_stage', isFile: true }])],
|
|
364
|
+
rm: [error(ioError({ message: 'busy' }))],
|
|
365
|
+
})(c.write(payload))
|
|
366
|
+
assert(result[0] === 'ok', ['expected write ok', result])
|
|
367
|
+
assertEq(log[0], 'now', ['expected the sweep to run first', log])
|
|
368
|
+
},
|
|
280
369
|
casWriteGcSkipsLiveLease: () => {
|
|
281
370
|
// A staging file whose deadline is still in the future is left alone by the GC
|
|
282
371
|
// that `write` runs before staging its own file.
|
|
@@ -288,55 +377,80 @@ export const proof = {
|
|
|
288
377
|
}
|
|
289
378
|
const content = vec8(0x2An)
|
|
290
379
|
const c = fileCas(sha256)('.')
|
|
291
|
-
const x = c.write(nonEmpty(
|
|
380
|
+
const x = c.write(nonEmpty(content, /** @satisfies {List<never, Vec, IoChannel>} */ (empty())))
|
|
292
381
|
const [state1, w] = virtual(state0)(x)
|
|
293
382
|
assert(w[0] === 'ok', ['expected write ok', w])
|
|
294
383
|
const [, present] = virtual(state1)(access(livePath))
|
|
295
384
|
assert(present[0] === 'ok', 'expected GC to leave the live staging file alone')
|
|
296
385
|
},
|
|
297
|
-
|
|
298
|
-
// A
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
386
|
+
casWriteBytesErrorAborts: () => {
|
|
387
|
+
// A `writeBytes` call failing mid-stream (disk full, permissions) fails closed:
|
|
388
|
+
// the partial staging file is deleted and the error is returned, without ever
|
|
389
|
+
// reaching a real filesystem here.
|
|
390
|
+
const c = fileCas(sha256)('.')
|
|
391
|
+
/** @type {List<never, Vec, IoChannel>} */
|
|
392
|
+
const payload = nonEmpty(vec8(0x11n), empty())
|
|
393
|
+
const [result, log] = drive({ writeBytes: [error(ioError({ message: 'disk full' }))] })(c.write(payload))
|
|
394
|
+
assertIoMessage(errorMessage(result), 'disk full')
|
|
395
|
+
// The cleanup `rm` of the partial staging file must actually run, not just be
|
|
396
|
+
// implied by the returned error tag.
|
|
397
|
+
assertEq(log[log.length - 1], 'rm', ['expected cleanup rm to run', log])
|
|
398
|
+
},
|
|
399
|
+
casWriteLeaseRenewalRenameErrorAborts: () => {
|
|
400
|
+
// The lease-renewal `rename` (after every chunk) failing fails the same way as a
|
|
401
|
+
// `writeBytes` failure: the partial staging file is deleted, error returned.
|
|
402
|
+
const c = fileCas(sha256)('.')
|
|
403
|
+
/** @type {List<never, Vec, IoChannel>} */
|
|
404
|
+
const payload = nonEmpty(vec8(0x11n), empty())
|
|
405
|
+
const [result, log] = drive({ rename: [error(ioError({ message: 'rename failed' }))] })(c.write(payload))
|
|
406
|
+
assertIoMessage(errorMessage(result), 'rename failed')
|
|
407
|
+
assertEq(log[log.length - 1], 'rm', ['expected cleanup rm to run', log])
|
|
408
|
+
},
|
|
409
|
+
casWritePublishSizeMismatchErrors: () => {
|
|
410
|
+
// `publish`'s three filesystem steps (mkdir/rename/rm) run best-effort with their
|
|
411
|
+
// results ignored; success is decided afterward by the final `stat`. A `stat`
|
|
412
|
+
// that reports `ok` but with a size other than what was written — a race with a
|
|
413
|
+
// concurrent writer — surfaces as a "publish size mismatch" error instead of a
|
|
414
|
+
// false "ok". Pins the size half of the check: a `stat` that returns `ok` with
|
|
415
|
+
// any size but the expected one must still fail, even though the tag alone says
|
|
416
|
+
// success.
|
|
417
|
+
const c = fileCas(sha256)('.')
|
|
418
|
+
/** @type {List<never, Vec, IoChannel>} */
|
|
419
|
+
const payload = nonEmpty(vec8(0x11n), empty())
|
|
420
|
+
const [result] = drive({ stat: [ok({ size: 999 })] })(c.write(payload))
|
|
421
|
+
assertIoMessage(errorMessage(result), 'publish size mismatch')
|
|
308
422
|
},
|
|
309
|
-
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
//
|
|
423
|
+
casWritePublishStatErrorErrorsEvenWithMatchingSize: () => {
|
|
424
|
+
// Pins the tag half of the same check: a `stat` that fails outright must still
|
|
425
|
+
// fail `write`, even in the degenerate case where its error payload happens to
|
|
426
|
+
// carry a `.size` field equal to the expected offset — otherwise a check that
|
|
427
|
+
// only compared `.size` (dropping the `st[0] === 'ok'` tag check) would slip
|
|
428
|
+
// through undetected by the size-mismatch case above, which never sees a
|
|
429
|
+
// coincidentally-matching size on a failed `stat`.
|
|
316
430
|
const c = fileCas(sha256)('.')
|
|
317
|
-
|
|
318
|
-
|
|
431
|
+
/** @type {List<never, Vec, IoChannel>} */
|
|
432
|
+
const payload = nonEmpty(vec8(0x11n), empty())
|
|
433
|
+
const [result] = drive({ stat: [error({ size: 1 })] })(c.write(payload))
|
|
434
|
+
assertIoMessage(errorMessage(result), 'publish size mismatch')
|
|
319
435
|
},
|
|
320
436
|
collectReadDrainsChunks: () => {
|
|
321
437
|
// The common path: every chunk is `ok`, so collectRead concatenates them all
|
|
322
438
|
// and returns the whole blob as one `Vec`.
|
|
323
|
-
/** @type {List<never,
|
|
324
|
-
const stream = nonEmpty(
|
|
439
|
+
/** @type {List<never, Vec, IoChannel>} */
|
|
440
|
+
const stream = nonEmpty(vec8(0x11n), nonEmpty(vec8(0x22n), empty()))
|
|
325
441
|
const o = runPure(collectRead(stream))
|
|
326
442
|
assert(o.length === 1, 'expected collectRead to finish without issuing a command')
|
|
327
443
|
assertEq(o[0][0], 'ok')
|
|
328
444
|
},
|
|
329
|
-
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const stream = nonEmpty(ok(vec8(0x11n)), nonEmpty(boom, /** @type {List<never, IoResult<Vec>>} */ (empty())))
|
|
445
|
+
collectReadPropagatesStreamFailure: () => {
|
|
446
|
+
// A stream that fails mid-way short-circuits collectRead with that same
|
|
447
|
+
// failure — `step` does it, so `collectRead` has no branch for it.
|
|
448
|
+
/** @type {List<never, Vec, IoChannel>} */
|
|
449
|
+
const stream = nonEmpty(vec8(0x11n), pureError(ioError({ message: 'boom' })))
|
|
335
450
|
const o = runPure(collectRead(stream))
|
|
336
451
|
assert(o.length === 1, 'expected collectRead to finish without issuing a command')
|
|
337
452
|
const [r] = o
|
|
338
|
-
|
|
339
|
-
assertEq(r[1], 'boom')
|
|
453
|
+
assert(r[0] === 'error' && r[1][0] === 'ioError' && r[1][1].message === 'boom', r)
|
|
340
454
|
},
|
|
341
455
|
// A single `Vec` cannot exceed `maxLength` bits — feed a pure stream whose second
|
|
342
456
|
// chunk pushes the running total just over the limit so the overflow guard fires
|
|
@@ -345,27 +459,25 @@ export const proof = {
|
|
|
345
459
|
const half = maxLength / 2n
|
|
346
460
|
const v1 = vec(half)(0n)
|
|
347
461
|
const v2 = vec(half + 1n)(0n)
|
|
348
|
-
/** @type {List<never,
|
|
349
|
-
const stream = nonEmpty(
|
|
462
|
+
/** @type {List<never, Vec, IoChannel>} */
|
|
463
|
+
const stream = nonEmpty(v1, nonEmpty(v2, empty()))
|
|
350
464
|
const o = runPure(collectRead(stream))
|
|
351
465
|
assert(o.length === 1, 'expected collectRead to finish without issuing a command')
|
|
352
466
|
assertEq(o[0][0], 'error')
|
|
353
467
|
},
|
|
354
468
|
casListPropagatesNonNotFoundAccessError: () => {
|
|
355
469
|
// A non-ENOENT `access` failure (permissions, corruption) is a genuine storage
|
|
356
|
-
// error and must propagate out of `list`,
|
|
470
|
+
// error and must propagate out of `list` as an `error`, neither swallowed as an
|
|
471
|
+
// empty store nor thrown: the caller decides what an unreadable store means.
|
|
357
472
|
const c = fileCas(sha256)('.')
|
|
358
|
-
const boom = { code: 'EACCES' }
|
|
473
|
+
const boom = ioError({ code: 'EACCES', message: 'permission denied' })
|
|
359
474
|
const r = casCommand(c.list())
|
|
360
475
|
assert(r[0] === 'cont', 'expected list() to issue an access command first')
|
|
361
476
|
assertEq(r[1], 'access')
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
threw = e
|
|
368
|
-
}
|
|
369
|
-
assertEq(threw, boom)
|
|
477
|
+
const answered = runPure(r[2](error(boom)))
|
|
478
|
+
assert(answered.length === 1, ['expected list() to answer without another command', answered])
|
|
479
|
+
const result = answered[0]
|
|
480
|
+
assert(result[0] === 'error', ['expected the access failure to propagate', result])
|
|
481
|
+
assertEq(result[1], boom)
|
|
370
482
|
},
|
|
371
483
|
}
|