mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-21 09:05:10 +00:00
Implement caching
Added caching util function with tests and applied in: - parser.Block.Transactions(), - parser.Payload.ChannelHeader(), - parser.Payload.SignatureHeader(), - parser.NamespaceReadWriteSet.ReadWriteSet(), - parser.EndorserTransaction.ReadWriteSets(), methods, as it was in the typescript sample. Corrected Println usage and added comments to util functions. Signed-off-by: Stanislav Jakuschevskij <stas@two-giants.com>
This commit is contained in:
parent
095bf304b4
commit
c4db079e0c
12 changed files with 158 additions and 73 deletions
|
|
@ -33,7 +33,7 @@ func main() {
|
||||||
printUsage()
|
printUsage()
|
||||||
panic(fmt.Errorf("unknown command: %s", name))
|
panic(fmt.Errorf("unknown command: %s", name))
|
||||||
}
|
}
|
||||||
fmt.Printf("command: %s\n", name)
|
fmt.Println("command:", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := newGrpcConnection()
|
client := newGrpcConnection()
|
||||||
|
|
@ -47,7 +47,7 @@ func main() {
|
||||||
|
|
||||||
func printUsage() {
|
func printUsage() {
|
||||||
fmt.Println("Arguments: <command1> [<command2> ...]")
|
fmt.Println("Arguments: <command1> [<command2> ...]")
|
||||||
fmt.Printf("Available commands: %v\n", availableCommands())
|
fmt.Println("Available commands:", availableCommands())
|
||||||
}
|
}
|
||||||
|
|
||||||
func availableCommands() string {
|
func availableCommands() string {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ func getAllAssets(clientConnection *grpc.ClientConn) {
|
||||||
smartContract := atb.NewAssetTransferBasic(contract)
|
smartContract := atb.NewAssetTransferBasic(contract)
|
||||||
assets := smartContract.GetAllAssets()
|
assets := smartContract.GetAllAssets()
|
||||||
|
|
||||||
fmt.Printf("%s\n", formatJSON(assets))
|
fmt.Println(formatJSON(assets))
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatJSON(data []byte) string {
|
func formatJSON(data []byte) string {
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,8 @@ func listen(clientConnection *grpc.ClientConn) {
|
||||||
}
|
}
|
||||||
defer checkpointer.Close()
|
defer checkpointer.Close()
|
||||||
|
|
||||||
fmt.Printf("Start event listening from block %d\n", checkpointer.BlockNumber())
|
fmt.Println("Start event listening from block", checkpointer.BlockNumber())
|
||||||
fmt.Printf("Last processed transaction ID within block: %s\n", checkpointer.TransactionID())
|
fmt.Println("Last processed transaction ID within block:", checkpointer.TransactionID())
|
||||||
if simulatedFailureCount > 0 {
|
if simulatedFailureCount > 0 {
|
||||||
fmt.Printf("Simulating a write failure every %d transactions\n", simulatedFailureCount)
|
fmt.Printf("Simulating a write failure every %d transactions\n", simulatedFailureCount)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,23 +22,38 @@ func (b *Block) Number() uint64 {
|
||||||
return header.GetNumber()
|
return header.GetNumber()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: needs cache
|
|
||||||
func (b *Block) Transactions() []*Transaction {
|
func (b *Block) Transactions() []*Transaction {
|
||||||
envelopes := b.unmarshalEnvelopesFromBlockData()
|
return utils.Cache(func() []*Transaction {
|
||||||
|
envelopes := b.unmarshalEnvelopesFromBlockData()
|
||||||
|
|
||||||
commonPayloads := b.unmarshalPayloadsFrom(envelopes)
|
commonPayloads := b.unmarshalPayloadsFrom(envelopes)
|
||||||
|
|
||||||
payloads := b.parse(commonPayloads)
|
payloads := b.parse(commonPayloads)
|
||||||
|
|
||||||
result := b.createTransactionsFrom(payloads)
|
return b.createTransactionsFrom(payloads)
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Block) unmarshalEnvelopesFromBlockData() []*common.Envelope {
|
||||||
|
result := []*common.Envelope{}
|
||||||
|
for _, blockData := range b.block.GetData().GetData() {
|
||||||
|
envelope := &common.Envelope{}
|
||||||
|
if err := proto.Unmarshal(blockData, envelope); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
result = append(result, envelope)
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Block) createTransactionsFrom(payloads []*PayloadImpl) []*Transaction {
|
func (*Block) unmarshalPayloadsFrom(envelopes []*common.Envelope) []*common.Payload {
|
||||||
result := []*Transaction{}
|
result := []*common.Payload{}
|
||||||
for _, payload := range payloads {
|
for _, envelope := range envelopes {
|
||||||
result = append(result, NewTransaction(payload))
|
commonPayload := &common.Payload{}
|
||||||
|
if err := proto.Unmarshal(envelope.GetPayload(), commonPayload); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
result = append(result, commonPayload)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
@ -62,30 +77,6 @@ func (b *Block) parse(commonPayloads []*common.Payload) []*PayloadImpl {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*Block) unmarshalPayloadsFrom(envelopes []*common.Envelope) []*common.Payload {
|
|
||||||
result := []*common.Payload{}
|
|
||||||
for _, envelope := range envelopes {
|
|
||||||
commonPayload := &common.Payload{}
|
|
||||||
if err := proto.Unmarshal(envelope.GetPayload(), commonPayload); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
result = append(result, commonPayload)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Block) unmarshalEnvelopesFromBlockData() []*common.Envelope {
|
|
||||||
result := []*common.Envelope{}
|
|
||||||
for _, blockData := range b.block.GetData().GetData() {
|
|
||||||
envelope := &common.Envelope{}
|
|
||||||
if err := proto.Unmarshal(blockData, envelope); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
result = append(result, envelope)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Block) extractTransactionValidationCodes() []byte {
|
func (b *Block) extractTransactionValidationCodes() []byte {
|
||||||
metadata := utils.AssertDefined(
|
metadata := utils.AssertDefined(
|
||||||
b.block.GetMetadata(),
|
b.block.GetMetadata(),
|
||||||
|
|
@ -98,6 +89,14 @@ func (b *Block) extractTransactionValidationCodes() []byte {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*Block) createTransactionsFrom(payloads []*PayloadImpl) []*Transaction {
|
||||||
|
result := []*Transaction{}
|
||||||
|
for _, payload := range payloads {
|
||||||
|
result = append(result, NewTransaction(payload))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// TODO remove unused?
|
// TODO remove unused?
|
||||||
func (b *Block) ToProto() *common.Block {
|
func (b *Block) ToProto() *common.Block {
|
||||||
return b.block
|
return b.block
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
"google.golang.org/protobuf/reflect/protoreflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetReadWriteSetsFromEndorserTransaction(t *testing.T) {
|
func Test_GetReadWriteSetsFromEndorserTransaction(t *testing.T) {
|
||||||
nsReadWriteSetFake, expectedNamespace, expectedAsset := nsReadWriteSetFake()
|
nsReadWriteSetFake, expectedNamespace, expectedAsset := nsReadWriteSetFake()
|
||||||
|
|
||||||
transaction := &peer.Transaction{
|
transaction := &peer.Transaction{
|
||||||
|
|
@ -73,7 +73,7 @@ func assertReadWriteSet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadWriteSetWrapping(t *testing.T) {
|
func Test_ReadWriteSetWrapping(t *testing.T) {
|
||||||
nsReadWriteSetFake, _, _ := nsReadWriteSetFake()
|
nsReadWriteSetFake, _, _ := nsReadWriteSetFake()
|
||||||
|
|
||||||
txReadWriteSetFake := &rwset.TxReadWriteSet{
|
txReadWriteSetFake := &rwset.TxReadWriteSet{
|
||||||
|
|
@ -86,7 +86,7 @@ func TestReadWriteSetWrapping(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNamespaceReadWriteSetParsing(t *testing.T) {
|
func Test_NamespaceReadWriteSetParsing(t *testing.T) {
|
||||||
nsReadWriteSetFake, expectedNamespace, expectedAsset := nsReadWriteSetFake()
|
nsReadWriteSetFake, expectedNamespace, expectedAsset := nsReadWriteSetFake()
|
||||||
|
|
||||||
parsedNsRwSet := parser.ParseNamespaceReadWriteSet(nsReadWriteSetFake)
|
parsedNsRwSet := parser.ParseNamespaceReadWriteSet(nsReadWriteSetFake)
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,16 @@ func ParsePayload(payload *common.Payload, statusCode int32) *PayloadImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PayloadImpl) ChannelHeader() *common.ChannelHeader {
|
func (p *PayloadImpl) ChannelHeader() *common.ChannelHeader {
|
||||||
header := utils.AssertDefined(p.payload.GetHeader(), "missing payload header")
|
return utils.Cache(func() *common.ChannelHeader {
|
||||||
|
header := utils.AssertDefined(p.payload.GetHeader(), "missing payload header")
|
||||||
|
|
||||||
// TODO add cache, return cachedChannelHeader like in blockParser.ts:77
|
result := &common.ChannelHeader{}
|
||||||
result := &common.ChannelHeader{}
|
if err := proto.Unmarshal(header.GetChannelHeader(), result); err != nil {
|
||||||
if err := proto.Unmarshal(header.GetChannelHeader(), result); err != nil {
|
panic(err)
|
||||||
panic(err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PayloadImpl) EndorserTransaction() EndorserTransaction {
|
func (p *PayloadImpl) EndorserTransaction() EndorserTransaction {
|
||||||
|
|
@ -55,15 +56,16 @@ func (p *PayloadImpl) EndorserTransaction() EndorserTransaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PayloadImpl) SignatureHeader() *common.SignatureHeader {
|
func (p *PayloadImpl) SignatureHeader() *common.SignatureHeader {
|
||||||
header := utils.AssertDefined(p.payload.GetHeader(), "missing payload header")
|
return utils.Cache(func() *common.SignatureHeader {
|
||||||
|
header := utils.AssertDefined(p.payload.GetHeader(), "missing payload header")
|
||||||
|
|
||||||
// TODO add cache, return cachedSignatureHeader like in blockParser.ts:77
|
result := &common.SignatureHeader{}
|
||||||
result := &common.SignatureHeader{}
|
if err := proto.Unmarshal(header.GetSignatureHeader(), result); err != nil {
|
||||||
if err := proto.Unmarshal(header.GetSignatureHeader(), result); err != nil {
|
panic(err)
|
||||||
panic(err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PayloadImpl) TransactionValidationCode() int32 {
|
func (p *PayloadImpl) TransactionValidationCode() int32 {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package parser
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"offChainData/utils"
|
||||||
|
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset"
|
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset"
|
||||||
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset/kvrwset"
|
"github.com/hyperledger/fabric-protos-go-apiv2/ledger/rwset/kvrwset"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
@ -53,14 +55,15 @@ func (p *NamespaceReadWriteSetImpl) Namespace() string {
|
||||||
return p.nsReadWriteSet.GetNamespace()
|
return p.nsReadWriteSet.GetNamespace()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add cache
|
|
||||||
func (p *NamespaceReadWriteSetImpl) ReadWriteSet() *kvrwset.KVRWSet {
|
func (p *NamespaceReadWriteSetImpl) ReadWriteSet() *kvrwset.KVRWSet {
|
||||||
result := kvrwset.KVRWSet{}
|
return utils.Cache(func() *kvrwset.KVRWSet {
|
||||||
if err := proto.Unmarshal(p.nsReadWriteSet.GetRwset(), &result); err != nil {
|
result := kvrwset.KVRWSet{}
|
||||||
panic(err)
|
if err := proto.Unmarshal(p.nsReadWriteSet.GetRwset(), &result); err != nil {
|
||||||
}
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
return &result
|
return &result
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO remove unused
|
// TODO remove unused
|
||||||
|
|
|
||||||
|
|
@ -71,21 +71,20 @@ func ParseEndorserTransaction(transaction *peer.Transaction) *EndorserTransactio
|
||||||
return &EndorserTransactionImpl{transaction}
|
return &EndorserTransactionImpl{transaction}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO add cache
|
|
||||||
func (p *EndorserTransactionImpl) ReadWriteSets() []ReadWriteSet {
|
func (p *EndorserTransactionImpl) ReadWriteSets() []ReadWriteSet {
|
||||||
chaincodeActionPayloads := p.unmarshalChaincodeActionPayloads()
|
return utils.Cache(func() []ReadWriteSet {
|
||||||
|
chaincodeActionPayloads := p.unmarshalChaincodeActionPayloads()
|
||||||
|
|
||||||
chaincodeEndorsedActions := p.extractChaincodeEndorsedActionsFrom(chaincodeActionPayloads)
|
chaincodeEndorsedActions := p.extractChaincodeEndorsedActionsFrom(chaincodeActionPayloads)
|
||||||
|
|
||||||
proposalResponsePayloads := p.unmarshalProposalResponsePayloadsFrom(chaincodeEndorsedActions)
|
proposalResponsePayloads := p.unmarshalProposalResponsePayloadsFrom(chaincodeEndorsedActions)
|
||||||
|
|
||||||
chaincodeActions := p.unmarshalChaincodeActionsFrom(proposalResponsePayloads)
|
chaincodeActions := p.unmarshalChaincodeActionsFrom(proposalResponsePayloads)
|
||||||
|
|
||||||
txReadWriteSets := p.unmarshalTxReadWriteSetsFrom(chaincodeActions)
|
txReadWriteSets := p.unmarshalTxReadWriteSetsFrom(chaincodeActions)
|
||||||
|
|
||||||
parsedReadWriteSets := p.parseReadWriteSets(txReadWriteSets)
|
return p.parseReadWriteSets(txReadWriteSets)
|
||||||
|
})()
|
||||||
return parsedReadWriteSets
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *EndorserTransactionImpl) unmarshalChaincodeActionPayloads() []*peer.ChaincodeActionPayload {
|
func (p *EndorserTransactionImpl) unmarshalChaincodeActionPayloads() []*peer.ChaincodeActionPayload {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ func (t *transactApp) transact() {
|
||||||
anAsset := atb.NewAsset()
|
anAsset := atb.NewAsset()
|
||||||
|
|
||||||
t.smartContract.CreateAsset(anAsset)
|
t.smartContract.CreateAsset(anAsset)
|
||||||
fmt.Printf("\nCreated asset %s\n", anAsset.ID)
|
fmt.Println("Created asset", anAsset.ID)
|
||||||
|
|
||||||
// Transfer randomly 1 in 2 assets to a new owner.
|
// Transfer randomly 1 in 2 assets to a new owner.
|
||||||
if utils.RandomInt(2) == 0 {
|
if utils.RandomInt(2) == 0 {
|
||||||
|
|
@ -56,6 +56,6 @@ func (t *transactApp) transact() {
|
||||||
// Delete randomly 1 in 4 created assets.
|
// Delete randomly 1 in 4 created assets.
|
||||||
if utils.RandomInt(4) == 0 {
|
if utils.RandomInt(4) == 0 {
|
||||||
t.smartContract.DeleteAsset(anAsset.ID)
|
t.smartContract.DeleteAsset(anAsset.ID)
|
||||||
fmt.Printf("Deleted asset %s\n", anAsset.ID)
|
fmt.Println("Deleted asset", anAsset.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,13 @@ import (
|
||||||
"math/big"
|
"math/big"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Pick a random element from an array.
|
||||||
func RandomElement(values []string) string {
|
func RandomElement(values []string) string {
|
||||||
result := values[RandomInt(len(values))]
|
result := values[RandomInt(len(values))]
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a random integer in the range 0 to max - 1.
|
||||||
func RandomInt(max int) int {
|
func RandomInt(max int) int {
|
||||||
result, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
result, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -20,6 +22,7 @@ func RandomInt(max int) int {
|
||||||
return int(result.Int64())
|
return int(result.Int64())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pick a random element from an array, excluding the current value.
|
||||||
func DifferentElement(values []string, currentValue string) string {
|
func DifferentElement(values []string, currentValue string) string {
|
||||||
candidateValues := []string{}
|
candidateValues := []string{}
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
|
|
@ -30,6 +33,7 @@ func DifferentElement(values []string, currentValue string) string {
|
||||||
return RandomElement(candidateValues)
|
return RandomElement(candidateValues)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the value if it is defined; otherwise panics with an error message.
|
||||||
func AssertDefined[T any](value T, message string) T {
|
func AssertDefined[T any](value T, message string) T {
|
||||||
if any(value) == any(nil) {
|
if any(value) == any(nil) {
|
||||||
panic(errors.New(message))
|
panic(errors.New(message))
|
||||||
|
|
@ -37,3 +41,17 @@ func AssertDefined[T any](value T, message string) T {
|
||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrap a function call with a cache. On first call the wrapped function is invoked to
|
||||||
|
// obtain a result. Subsequent calls return the cached result.
|
||||||
|
func Cache[T any](f func() T) func() T {
|
||||||
|
value := any(nil)
|
||||||
|
|
||||||
|
return func() T {
|
||||||
|
if value == nil {
|
||||||
|
value = f()
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.(T)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
64
off_chain_data/application-go/utils/utils_test.go
Normal file
64
off_chain_data/application-go/utils/utils_test.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package utils_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"offChainData/utils"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_cachePrimitiveFunctionResult(t *testing.T) {
|
||||||
|
counter := 0
|
||||||
|
f := func() int {
|
||||||
|
counter++
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedFunc := utils.Cache(f)
|
||||||
|
result1 := cachedFunc()
|
||||||
|
result2 := cachedFunc()
|
||||||
|
|
||||||
|
if counter != 1 {
|
||||||
|
t.Error("expected counter to be 1, but got", counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result1 != 5 || result2 != 5 {
|
||||||
|
t.Fatal("expected results to be 5, but got", result1, result2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_cacheWrappedPrimitiveFunctionResult(t *testing.T) {
|
||||||
|
controlValue := 5
|
||||||
|
multiplyControlValueBy := func(n int) int { controlValue *= n; return controlValue }
|
||||||
|
|
||||||
|
cachedFunc := utils.Cache(func() int { return multiplyControlValueBy(5) })
|
||||||
|
result1 := cachedFunc()
|
||||||
|
result2 := cachedFunc()
|
||||||
|
|
||||||
|
if controlValue != 25 {
|
||||||
|
t.Error("expected control value to be 25, but got", controlValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result1 != 25 || result2 != 25 {
|
||||||
|
t.Fatal("expected cached results to be 25, but got", result1, result2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_cacheWrappedDataStructureResult(t *testing.T) {
|
||||||
|
type GreetMe struct {
|
||||||
|
helloTo string
|
||||||
|
}
|
||||||
|
|
||||||
|
controlStruct := &GreetMe{helloTo: "Hello "}
|
||||||
|
greet := func(name string) *GreetMe { controlStruct.helloTo += name; return controlStruct }
|
||||||
|
|
||||||
|
cachedFunc := utils.Cache(func() *GreetMe { return greet("John Doe") })
|
||||||
|
result1 := cachedFunc().helloTo
|
||||||
|
result2 := cachedFunc().helloTo
|
||||||
|
|
||||||
|
if controlStruct.helloTo != "Hello John Doe" {
|
||||||
|
t.Error("expected control value to be 'Hello John Doe', but got", controlStruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result1 != "Hello John Doe" || result2 != "Hello John Doe" {
|
||||||
|
t.Fatal("expected cached results to be 'Hello John Doe', but got", result1, result2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -67,7 +67,7 @@ export function assertDefined<T>(value: T | null | undefined, message: string):
|
||||||
*/
|
*/
|
||||||
export function cache<T>(f: () => T): () => T {
|
export function cache<T>(f: () => T): () => T {
|
||||||
let value: T | undefined;
|
let value: T | undefined;
|
||||||
return () => {
|
return (): T => {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
value = f();
|
value = f();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue