diff --git a/card.go b/card.go
index d566fd9..449ad67 100644
--- a/card.go
+++ b/card.go
@@ -16,7 +16,13 @@
package riff
-import "time"
+import (
+ "encoding/json"
+ "time"
+
+ "github.com/open-spaced-repetition/go-fsrs"
+ "github.com/siyuan-note/logging"
+)
// Card 描述了闪卡。
type Card interface {
@@ -24,7 +30,10 @@ type Card interface {
ID() string
// BlockID 返回闪卡关联的内容块 ID。
- BlockID() string
+ // BlockID() string
+
+ // CSID 获取卡片的 CSID
+ GetCSID() string
// NextDues 返回每种评分对应的下次到期时间。
NextDues() map[Rating]time.Time
@@ -32,23 +41,72 @@ type Card interface {
// SetNextDues 设置每种评分对应的下次到期时间。
SetNextDues(map[Rating]time.Time)
- // SetDue 设置到期时间。
- SetDue(time.Time)
+ // GetUpdate 返回闪卡的更新时间。
+ GetUpdate() time.Time
+
+ // SetUpdate 设置闪卡的更新时间。
+ SetUpdate(time.Time)
+
+ // GetState 返回闪卡状态。
+ GetState() State
+
+ // SetState 设置闪卡状态。
+ SetState(State)
// GetLapses 返回闪卡的遗忘次数。
GetLapses() int
+ // SetLapses 设置闪卡的遗忘次数。
+ SetLapses(int)
+
// GetReps 返回闪卡的复习次数。
GetReps() int
- // GetState 返回闪卡状态。
- GetState() State
+ // SetReps 设置闪卡的复习次数。
+ SetReps(int)
+
+ // GetSuspend 返回闪卡是否被暂停。
+ GetSuspend() bool
+
+ // SetSuspend 设置闪卡是否被暂停。
+ SetSuspend(bool)
+
+ // GetTag 返回闪卡的标签。
+ GetTag() string
+
+ // SetTag 设置闪卡的标签。
+ SetTag(string)
+
+ // GetFlag 返回闪卡的标志。
+ GetFlag() string
+
+ // SetFlag 设置闪卡的标志。
+ SetFlag(string)
+
+ // GetPriority 返回闪卡的优先级。
+ GetPriority() float64
+
+ // SetPriority 设置闪卡的优先级。
+ SetPriority(float64)
+
+ // GetDue 返回闪卡的到期时间。
+ GetDue() time.Time
+
+ // SetDue 设置闪卡的到期时间。
+ SetDue(time.Time)
+
+ // 返回 Algo
+ GetAlgo() Algo
+
+ UseAlgo(algo Algo)
- // GetLastReview 返回闪卡的最后复习时间。
- GetLastReview() time.Time
+ // 返回 MarshalImpl
+ GetMarshalImpl() []uint8
- // Clone 返回闪卡的克隆。
- Clone() Card
+ // 对 Impl 进行 Marshal
+ MarshalImpl()
+
+ UnmarshalImpl()
// Impl 返回具体的闪卡实现。
Impl() interface{}
@@ -57,11 +115,129 @@ type Card interface {
SetImpl(c interface{})
}
+func UnmarshalImpl(card Card) {
+ switch card.GetAlgo() {
+ case AlgoFSRS:
+ impl := fsrs.Card{}
+ json.Unmarshal(card.GetMarshalImpl(), &impl)
+ card.SetImpl(impl)
+ default:
+ return
+ }
+}
+
// BaseCard 描述了基础的闪卡实现。
type BaseCard struct {
- CID string
- BID string
- NDues map[Rating]time.Time
+ CID string `xorm:"pk index"`
+ CSID string
+ Update time.Time
+ State State //State 返回闪卡状态。
+ Lapses int //Lapses 返回闪卡的遗忘次数。
+ Reps int //Reps 返回闪卡的复习次数。
+ Suspend bool `xorm:"index"`
+ Tag string
+ Flag string
+ Priority float64
+ Due time.Time `xorm:"index"`
+ NDues map[Rating]time.Time `xorm:"-"`
+ Algo Algo
+ AlgoImpl interface{} `xorm:"-"`
+ AlgoImplData []uint8 `json:"-"`
+}
+
+func NewBaseCard(cs CardSource) (card *BaseCard) {
+ CSID := cs.GetCSID()
+ card = &BaseCard{
+ CSID: CSID,
+ CID: newID(),
+ State: New,
+ Update: time.Now(),
+ Due: time.Now(),
+ NDues: map[Rating]time.Time{},
+ Priority: 0.5,
+ AlgoImplData: []uint8{},
+ }
+ return
+}
+
+func (card *BaseCard) ID() string {
+ return card.CID
+}
+
+func (card *BaseCard) GetCSID() string {
+ return card.CSID
+}
+
+func (c *BaseCard) GetUpdate() time.Time {
+ return c.Update
+}
+
+func (c *BaseCard) SetUpdate(update time.Time) {
+ c.Update = update
+}
+
+func (c *BaseCard) GetState() State {
+ return c.State
+}
+
+func (c *BaseCard) SetState(state State) {
+ c.State = state
+}
+
+func (c *BaseCard) GetLapses() int {
+ return c.Lapses
+}
+
+func (c *BaseCard) SetLapses(lapses int) {
+ c.Lapses = lapses
+}
+
+func (c *BaseCard) GetReps() int {
+ return c.Reps
+}
+
+func (c *BaseCard) SetReps(reps int) {
+ c.Reps = reps
+}
+
+func (c *BaseCard) GetSuspend() bool {
+ return c.Suspend
+}
+
+func (c *BaseCard) SetSuspend(suspend bool) {
+ c.Suspend = suspend
+}
+
+func (c *BaseCard) GetTag() string {
+ return c.Tag
+}
+
+func (c *BaseCard) SetTag(tag string) {
+ c.Tag = tag
+}
+
+func (c *BaseCard) GetFlag() string {
+ return c.Flag
+}
+
+func (c *BaseCard) SetFlag(flag string) {
+ c.Flag = flag
+}
+
+func (c *BaseCard) GetPriority() float64 {
+ return c.Priority
+}
+
+func (c *BaseCard) SetPriority(priority float64) {
+ c.Priority = priority
+}
+
+func (c *BaseCard) GetDue() time.Time {
+ return c.Due
+}
+
+func (c *BaseCard) SetDue(due time.Time) {
+ c.Due = due
}
func (card *BaseCard) NextDues() map[Rating]time.Time {
@@ -72,10 +248,42 @@ func (card *BaseCard) SetNextDues(dues map[Rating]time.Time) {
card.NDues = dues
}
-func (card *BaseCard) ID() string {
- return card.CID
+func (card *BaseCard) Impl() interface{} {
+ return card.AlgoImpl
+}
+func (card *BaseCard) SetImpl(c interface{}) {
+ card.AlgoImpl = c
+}
+
+func (card *BaseCard) UseAlgo(algo Algo) {
+ switch algo {
+ case AlgoFSRS:
+ AlgoImpl := fsrs.NewCard()
+ card.AlgoImpl = AlgoImpl
+ card.Algo = AlgoFSRS
+ // card.Due = AlgoImpl.Due
+ default:
+ logging.LogErrorf("unsupported Algo: %s", algo)
+ }
+}
+
+func (card *BaseCard) GetMarshalImpl() []uint8 {
+ if card.AlgoImplData == nil || len(card.AlgoImplData) == 0 {
+ card.MarshalImpl()
+ }
+
+ return card.AlgoImplData
+}
+
+func (card *BaseCard) MarshalImpl() {
+ data, _ := json.Marshal(card.AlgoImpl)
+ card.AlgoImplData = data
+}
+
+func (card *BaseCard) UnmarshalImpl() {
+ UnmarshalImpl(card)
}
-func (card *BaseCard) BlockID() string {
- return card.BID
+func (card *BaseCard) GetAlgo() Algo {
+ return card.Algo
}
diff --git a/card_history.go b/card_history.go
new file mode 100644
index 0000000..bd10bda
--- /dev/null
+++ b/card_history.go
@@ -0,0 +1,108 @@
+package riff
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/open-spaced-repetition/go-fsrs"
+)
+
+type History interface {
+ ID() string
+
+ // 对 Impl 进行 Marshal
+ MarshalImpl()
+
+ UnmarshalImpl() (err error)
+}
+
+type BaseHistory struct {
+ HID string `xorm:"pk index"`
+ CID string
+ Update time.Time `xorm:"index"`
+ UpdateResult int
+ State State
+ Tag string
+ Flag string
+ Suspend bool
+ Priority float64
+ Due time.Time
+ Algo Algo
+ AlgoImpl interface{} `xorm:"-"`
+ AlgoImplData []uint8 `json:"-"`
+}
+
+type ReviewLog struct {
+ HID string
+ Rate Rating
+ Review time.Time
+}
+
+type ReviewHistory struct {
+ BaseHistory `xorm:"extends"`
+ ReviewLog `xorm:"extends"`
+}
+
+func NewBaseHistory(c Card) (history *BaseHistory) {
+ return &BaseHistory{
+ HID: newID(),
+ CID: c.ID(),
+ Update: time.Now(),
+ UpdateResult: CreateHistory, // 这里可以根据需要设置具体的更新结果
+ State: c.GetState(),
+ Tag: c.GetTag(),
+ Flag: c.GetFlag(),
+ Suspend: c.GetSuspend(),
+ Priority: c.GetPriority(),
+ Due: c.GetDue(),
+ Algo: c.GetAlgo(),
+ AlgoImpl: c.Impl(), // 假设Card接口有Impl方法
+ AlgoImplData: c.GetMarshalImpl(), // 假设Card接口有GetMarshalImpl方法
+ }
+}
+
+func (b *BaseHistory) ID() string {
+ return b.HID
+}
+
+func (b *BaseHistory) UnmarshalImpl() (err error) {
+ if b.AlgoImplData == nil || len(b.AlgoImplData) == 0 {
+ err = errors.New("dont have AlgoImplData")
+ return
+ }
+ switch b.Algo {
+ case AlgoFSRS:
+ impl := fsrs.Card{}
+ json.Unmarshal(b.AlgoImplData, &impl)
+ b.AlgoImpl = impl
+ default:
+ err = fmt.Errorf("un support Algo Type : %s", string(b.Algo))
+ return
+ }
+ return
+}
+
+func (b *BaseHistory) MarshalImpl() {
+ if len(b.AlgoImplData) != 0 && b.AlgoImpl == nil {
+ return
+ }
+ data, _ := json.Marshal(b.AlgoImpl)
+ b.AlgoImplData = data
+}
+
+func NewReviewLog(h History, rate Rating) (log *ReviewLog) {
+ log = &ReviewLog{
+ HID: h.ID(),
+ Rate: rate,
+ Review: time.Now(),
+ }
+ return
+}
+
+const (
+ EditUpdate int = -2
+ CreateHistory int = -1
+ ReviewUpdate int = 1
+)
diff --git a/card_source.go b/card_source.go
new file mode 100644
index 0000000..04ef8f4
--- /dev/null
+++ b/card_source.go
@@ -0,0 +1,38 @@
+package riff
+
+type CardSource interface {
+ GetCSID() string
+ GetDIDs() []string
+ GetBlockIDs() []string
+}
+
+type BaseCardSource struct {
+ CSID string `xorm:"pk index"`
+ Hash string
+ BlockIDs []string
+ DID []string
+ CType string
+ SourceContext map[string]interface{}
+}
+
+func NewBaseCardSource(DID string) *BaseCardSource {
+ ID := newID()
+ cardSource := &BaseCardSource{
+ CSID: ID,
+ DID: []string{DID},
+ BlockIDs: []string{},
+ SourceContext: map[string]interface{}{},
+ }
+ return cardSource
+}
+
+func (cs *BaseCardSource) GetCSID() string {
+ return cs.CSID
+}
+
+func (cs *BaseCardSource) GetDIDs() []string {
+ return cs.DID
+}
+func (cs *BaseCardSource) GetBlockIDs() []string {
+ return cs.BlockIDs
+}
diff --git a/deck.go b/deck.go
index ebc9372..707f79f 100644
--- a/deck.go
+++ b/deck.go
@@ -16,208 +16,134 @@
package riff
-import (
- "errors"
- "path/filepath"
- "sync"
- "time"
-
- "github.com/siyuan-note/filelock"
- "github.com/siyuan-note/logging"
- "github.com/vmihailenco/msgpack/v5"
+import "time"
+
+type Deck interface {
+ AddCard(cardID, blockID string) // FINISH
+ RemoveCard(cardID string) // TODO
+ SetCard(card Card) // TODO
+ GetCard(cardID string) Card // TODO
+ GetCardsByBlockID(blockID string) (ret []Card) // UNKNOW
+ GetCardsByBlockIDs(blockIDs []string) (ret []Card) // INSTEAD OF BETTER
+ GetNewCardsByBlockIDs(blockIDs []string) (ret []Card) // INSTEAD OF BETTER
+ GetDueCardsByBlockIDs(blockIDs []string) (ret []Card) // INSTEAD OF BETTER
+ GetBlockIDs() (ret []string) // TODO
+ CountCards() int // TODO
+ Save() (err error) // FINISH
+ SaveLog(log *Log) (err error) // FINISH
+ Review(cardID string, rating Rating) (ret *Log) // FINISH
+ Dues() (ret []Card) // FINISH
+ GetDID() string
+}
+
+// BaseDeck 描述了一套闪卡包。
+type BaseDeck struct {
+ DID string // DeckID
+ Name string // 名称
+ Desc string // 描述
+ DeckType DeckType
+ Created time.Time // 创建时间
+ Updated time.Time `xorm:"updated"` // 更新时间
+ ParentDeckID string
+ DeckContext map[string]interface{}
+ riff *Riff `json:"-"`
+}
+
+type DeckType string
+
+const (
+ Set DeckType = "Set"
+ Collection DeckType = "Collection"
)
-// Deck 描述了一套闪卡包。
-type Deck struct {
- ID string // ID
- Name string // 名称
- Algo Algo // 间隔重复算法
- Desc string // 描述
- Created int64 // 创建时间
- Updated int64 // 更新时间
-
- store Store // 底层存储
- lock *sync.Mutex
-}
-
-// LoadDeck 从文件夹 saveDir 路径上加载 id 闪卡包。
-func LoadDeck(saveDir, id string, requestRetention float64, maximumInterval int, weights string) (deck *Deck, err error) {
- created := time.Now().UnixMilli()
- deck = &Deck{
- ID: id,
- Name: id,
- Algo: AlgoFSRS,
- Created: created,
- Updated: created,
- lock: &sync.Mutex{},
- }
-
- dataPath := getDeckMsgpackPath(saveDir, id)
- if filelock.IsExist(dataPath) {
- var data []byte
- data, err = filelock.ReadFile(dataPath)
- if nil != err {
- logging.LogErrorf("load deck [%s] failed: %s", deck.Name, err)
- return
- }
-
- err = msgpack.Unmarshal(data, deck)
- if nil != err {
- logging.LogErrorf("load deck [%s] failed: %s", deck.Name, err)
- return
- }
+func DefaultBaseDeck() *BaseDeck {
+ Created := time.Now()
+ deck := &BaseDeck{
+ DID: builtInDeck,
+ Name: "builtInDeck",
+ Desc: "built in Deck",
+ Created: Created,
+ DeckType: Collection,
+ DeckContext: map[string]interface{}{},
}
+ return deck
+}
- var store Store
- switch deck.Algo {
- case AlgoFSRS:
- store = NewFSRSStore(deck.ID, saveDir, requestRetention, maximumInterval, weights)
- err = store.Load()
- default:
- err = errors.New("not supported yet")
- return
- }
- if nil != err {
- return
+func NewBaseDeck() (deck *BaseDeck) {
+ deck = &BaseDeck{
+ DID: newID(),
+ Created: time.Now(),
+ DeckType: Collection,
}
- deck.store = store
return
}
-// AddCard 新建一张闪卡。
-func (deck *Deck) AddCard(cardID, blockID string) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- card := deck.store.GetCard(cardID)
- if nil != card {
- return
- }
-
- deck.store.AddCard(cardID, blockID)
- deck.Updated = time.Now().UnixMilli()
+func (bd *BaseDeck) AddCard(cardID, blockID string) {
+ // 空实现
}
-// RemoveCard 删除一张闪卡。
-func (deck *Deck) RemoveCard(cardID string) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- deck.store.RemoveCard(cardID)
- deck.Updated = time.Now().UnixMilli()
+func (bd *BaseDeck) RemoveCard(cardID string) {
+ // 空实现
}
-// SetCard 设置一张闪卡。
-func (deck *Deck) SetCard(card Card) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- deck.store.SetCard(card)
+func (bd *BaseDeck) SetCard(card Card) {
+ // 空实现
}
-// GetCard 根据闪卡 ID 获取对应的闪卡。
-func (deck *Deck) GetCard(cardID string) Card {
- deck.lock.Lock()
- defer deck.lock.Unlock()
- return deck.store.GetCard(cardID)
+func (bd *BaseDeck) GetCard(cardID string) Card {
+ // 空实现
+ return nil
}
-func (deck *Deck) GetCardsByBlockID(blockID string) (ret []Card) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- return deck.store.GetCardsByBlockID(blockID)
+func (bd *BaseDeck) GetCardsByBlockID(blockID string) (ret []Card) {
+ // 空实现
+ return nil
}
-// GetCardsByBlockIDs 获取指定内容块的所有卡片。
-func (deck *Deck) GetCardsByBlockIDs(blockIDs []string) (ret []Card) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- return deck.store.GetCardsByBlockIDs(blockIDs)
+func (bd *BaseDeck) GetCardsByBlockIDs(blockIDs []string) (ret []Card) {
+ // 空实现
+ return nil
}
-func (deck *Deck) GetNewCardsByBlockIDs(blockIDs []string) (ret []Card) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- return deck.store.GetNewCardsByBlockIDs(blockIDs)
+func (bd *BaseDeck) GetNewCardsByBlockIDs(blockIDs []string) (ret []Card) {
+ // 空实现
+ return nil
}
-func (deck *Deck) GetDueCardsByBlockIDs(blockIDs []string) (ret []Card) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- return deck.store.GetDueCardsByBlockIDs(blockIDs)
+func (bd *BaseDeck) GetDueCardsByBlockIDs(blockIDs []string) (ret []Card) {
+ // 空实现
+ return nil
}
-// GetBlockIDs 获取所有内容块 ID。
-func (deck *Deck) GetBlockIDs() (ret []string) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- return deck.store.GetBlockIDs()
+func (bd *BaseDeck) GetBlockIDs() (ret []string) {
+ // 空实现
+ return nil
}
-// CountCards 获取卡包中的闪卡数量。
-func (deck *Deck) CountCards() int {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- return deck.store.CountCards()
+func (bd *BaseDeck) CountCards() int {
+ // 空实现
+ return 0
}
-// Save 保存闪卡包。
-func (deck *Deck) Save() (err error) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- deck.Updated = time.Now().UnixMilli()
- err = deck.store.Save()
- if nil != err {
- logging.LogErrorf("save deck [%s] failed: %s", deck.Name, err)
- return
- }
-
- saveDir := deck.store.GetSaveDir()
- dataPath := getDeckMsgpackPath(saveDir, deck.ID)
- data, err := msgpack.Marshal(deck)
- if nil != err {
- logging.LogErrorf("save deck failed: %s", err)
- return
- }
- if err = filelock.WriteFile(dataPath, data); nil != err {
- logging.LogErrorf("save deck failed: %s", err)
- return
- }
- return
+func (bd *BaseDeck) Save() (err error) {
+ // 空实现
+ return nil
}
-// SaveLog 保存闪卡包的复习日志。
-func (deck *Deck) SaveLog(log *Log) (err error) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- return deck.store.SaveLog(log)
+func (bd *BaseDeck) SaveLog(log *Log) (err error) {
+ // 空实现
+ return nil
}
-// Review 复习一张闪卡,rating 为复习评分结果。
-func (deck *Deck) Review(cardID string, rating Rating) (ret *Log) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
-
- ret = deck.store.Review(cardID, rating)
- deck.Updated = time.Now().UnixMilli()
- return
+func (bd *BaseDeck) Review(cardID string, rating Rating) (ret *Log) {
+ // 空实现
+ return nil
}
-// Dues 返回所有到期的闪卡。
-func (deck *Deck) Dues() (ret []Card) {
- deck.lock.Lock()
- defer deck.lock.Unlock()
- return deck.store.Dues()
+func (bd *BaseDeck) Dues() (ret []Card) {
+ // 空实现
+ return nil
}
-
-func getDeckMsgpackPath(saveDir, id string) string {
- return filepath.Join(saveDir, id+".deck")
+func (bd *BaseDeck) GetDID() string {
+ return bd.DID
}
diff --git a/deck_test.go b/deck_test.go
deleted file mode 100644
index acffcf5..0000000
--- a/deck_test.go
+++ /dev/null
@@ -1,83 +0,0 @@
-// Riff - Spaced repetition.
-// Copyright (c) 2022-present, b3log.org
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package riff
-
-import (
- "os"
- "testing"
-
- "github.com/open-spaced-repetition/go-fsrs"
-)
-
-func TestDeck(t *testing.T) {
- const saveDir = "testdata"
- os.MkdirAll(saveDir, 0755)
- defer os.RemoveAll(saveDir)
- deckID := newID()
- deck, err := LoadDeck(saveDir, deckID, requestRetention, maximumInterval, weights)
- if nil != err {
- t.Fatal(err)
- }
- deckName := "deck0"
- if deck.Name == deckID {
- deck.Name = deckName
- }
-
- cardID, blockID := newID(), newID()
- deck.AddCard(cardID, blockID)
- card := deck.GetCard(cardID)
- if card.ID() != cardID {
- t.Fatalf("card id [%s] != [%s]", card.ID(), cardID)
- }
-
- deck.Review(cardID, Good)
- due := card.Impl().(*fsrs.Card).Due.UnixMilli()
- card = deck.GetCard(cardID)
- due2 := card.Impl().(*fsrs.Card).Due.UnixMilli()
- if due2 != due {
- t.Fatalf("card due [%v] != [%v]", due2, due)
- }
-
- err = deck.Save()
- if nil != err {
- t.Fatal(err)
- }
- deck = nil
-
- deck, err = LoadDeck(saveDir, deckID, requestRetention, maximumInterval, weights)
- if nil != err {
- t.Fatal(err)
- }
-
- if deckName != deck.Name {
- t.Fatalf("deck name [%s] != [%s]", deck.Name, deckID)
- }
-
- card = deck.GetCard(cardID)
- if card.ID() != cardID {
- t.Fatalf("card id [%s] != [%s]", card.ID(), cardID)
- }
- due3 := card.Impl().(*fsrs.Card).Due.UnixMilli()
- if due2 != due3 {
- t.Fatalf("card due [%v] != [%v]", due2, due3)
- }
-
- count := deck.CountCards()
- if 1 != count {
- t.Fatalf("card count [%d] != [1]", count)
- }
-}
diff --git a/fsrs_store.go b/fsrs_store.go
deleted file mode 100644
index 9a588e4..0000000
--- a/fsrs_store.go
+++ /dev/null
@@ -1,362 +0,0 @@
-// Riff - Spaced repetition.
-// Copyright (c) 2022-present, b3log.org
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package riff
-
-import (
- "os"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "time"
-
- "github.com/88250/gulu"
- "github.com/open-spaced-repetition/go-fsrs"
- "github.com/siyuan-note/filelock"
- "github.com/siyuan-note/logging"
- "github.com/vmihailenco/msgpack/v5"
-)
-
-type FSRSStore struct {
- *BaseStore
-
- cards map[string]*FSRSCard
- params fsrs.Parameters
-}
-
-func NewFSRSStore(id, saveDir string, requestRetention float64, maximumInterval int, weights string) *FSRSStore {
- params := fsrs.DefaultParam()
- params.RequestRetention = requestRetention
- params.MaximumInterval = float64(maximumInterval)
- params.W = [17]float64{}
- for i, w := range strings.Split(weights, ",") {
- w = strings.TrimSpace(w)
- params.W[i], _ = strconv.ParseFloat(w, 64)
- }
-
- return &FSRSStore{
- BaseStore: NewBaseStore(id, "fsrs", saveDir),
- cards: map[string]*FSRSCard{},
- params: params,
- }
-}
-
-func (store *FSRSStore) AddCard(id, blockID string) Card {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- c := fsrs.NewCard()
- card := &FSRSCard{BaseCard: &BaseCard{id, blockID, nil}, C: &c}
- store.cards[id] = card
- return card
-}
-
-func (store *FSRSStore) GetCard(id string) Card {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- ret := store.cards[id]
- if nil == ret {
- return nil
- }
- return ret
-}
-
-func (store *FSRSStore) SetCard(card Card) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- store.cards[card.ID()] = card.(*FSRSCard)
-}
-
-func (store *FSRSStore) RemoveCard(id string) Card {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- card := store.cards[id]
- if nil == card {
- return nil
- }
- delete(store.cards, id)
- return card
-}
-
-func (store *FSRSStore) GetCardsByBlockID(blockID string) (ret []Card) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- for _, card := range store.cards {
- if card.BlockID() == blockID {
- ret = append(ret, card)
- }
- }
- return
-}
-
-func (store *FSRSStore) GetCardsByBlockIDs(blockIDs []string) (ret []Card) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs)
- for _, card := range store.cards {
- if gulu.Str.Contains(card.BlockID(), blockIDs) {
- ret = append(ret, card)
- }
- }
- return
-}
-
-func (store *FSRSStore) GetNewCardsByBlockIDs(blockIDs []string) (ret []Card) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs)
- for _, card := range store.cards {
- c := card.Impl().(*fsrs.Card)
- if !c.LastReview.IsZero() {
- continue
- }
-
- if gulu.Str.Contains(card.BlockID(), blockIDs) {
- ret = append(ret, card)
- }
- }
- return
-}
-
-func (store *FSRSStore) GetDueCardsByBlockIDs(blockIDs []string) (ret []Card) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs)
- now := time.Now()
- for _, card := range store.cards {
- c := card.Impl().(*fsrs.Card)
- if now.Before(c.Due) {
- continue
- }
-
- if gulu.Str.Contains(card.BlockID(), blockIDs) {
- ret = append(ret, card)
- }
- }
- return
-}
-
-func (store *FSRSStore) GetBlockIDs() (ret []string) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- ret = []string{}
- for _, card := range store.cards {
- ret = append(ret, card.BlockID())
- }
- ret = gulu.Str.RemoveDuplicatedElem(ret)
- sort.Strings(ret)
- return
-}
-
-func (store *FSRSStore) CountCards() int {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- return len(store.cards)
-}
-
-func (store *FSRSStore) Review(cardId string, rating Rating) (ret *Log) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- now := time.Now()
- card := store.cards[cardId]
- if nil == card {
- logging.LogWarnf("not found card [id=%s] to review", cardId)
- return
- }
-
- schedulingInfo := store.params.Repeat(*card.C, now)
- updated := schedulingInfo[fsrs.Rating(rating)].Card
- card.SetImpl(&updated)
- store.cards[cardId] = card
-
- reviewLog := schedulingInfo[fsrs.Rating(rating)].ReviewLog
- ret = &Log{
- ID: newID(),
- CardID: cardId,
- Rating: rating,
- ScheduledDays: reviewLog.ScheduledDays,
- ElapsedDays: reviewLog.ElapsedDays,
- Reviewed: reviewLog.Review.Unix(),
- State: State(reviewLog.State),
- }
- return
-}
-
-func (store *FSRSStore) Dues() (ret []Card) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- now := time.Now()
- for _, card := range store.cards {
- c := card.Impl().(*fsrs.Card)
- if now.Before(c.Due) {
- continue
- }
-
- schedulingInfos := store.params.Repeat(*c, now)
- nextDues := map[Rating]time.Time{}
- for rating, schedulingInfo := range schedulingInfos {
- nextDues[Rating(rating)] = schedulingInfo.Card.Due
- }
- card.SetNextDues(nextDues)
- ret = append(ret, card)
- }
- return
-}
-
-func (store *FSRSStore) Load() (err error) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- store.cards = map[string]*FSRSCard{}
- p := store.getMsgPackPath()
- if !filelock.IsExist(p) {
- return
- }
-
- data, err := filelock.ReadFile(p)
- if nil != err {
- logging.LogErrorf("load cards failed: %s", err)
- }
- if err = msgpack.Unmarshal(data, &store.cards); nil != err {
- logging.LogErrorf("load cards failed: %s", err)
- return
- }
- return
-}
-
-func (store *FSRSStore) Save() (err error) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- saveDir := store.GetSaveDir()
- if !gulu.File.IsDir(saveDir) {
- if err = os.MkdirAll(saveDir, 0755); nil != err {
- return
- }
- }
-
- p := store.getMsgPackPath()
- data, err := msgpack.Marshal(store.cards)
- if nil != err {
- logging.LogErrorf("save cards failed: %s", err)
- return
- }
- if err = filelock.WriteFile(p, data); nil != err {
- logging.LogErrorf("save cards failed: %s", err)
- return
- }
- return
-}
-
-func (store *FSRSStore) SaveLog(log *Log) (err error) {
- store.lock.Lock()
- defer store.lock.Unlock()
-
- saveDir := store.GetSaveDir()
- saveDir = filepath.Join(saveDir, "logs")
- if !gulu.File.IsDir(saveDir) {
- if err = os.MkdirAll(saveDir, 0755); nil != err {
- return
- }
- }
-
- yyyyMM := time.Now().Format("200601")
- p := filepath.Join(saveDir, yyyyMM+".msgpack")
- logs := []*Log{}
- var data []byte
- if filelock.IsExist(p) {
- data, err = filelock.ReadFile(p)
- if nil != err {
- logging.LogErrorf("load logs failed: %s", err)
- return
- }
-
- if err = msgpack.Unmarshal(data, &logs); nil != err {
- logging.LogErrorf("unmarshal logs failed: %s", err)
- return
- }
- }
- logs = append(logs, log)
-
- if data, err = msgpack.Marshal(logs); nil != err {
- logging.LogErrorf("marshal logs failed: %s", err)
- return
- }
- if err = filelock.WriteFile(p, data); nil != err {
- logging.LogErrorf("write logs failed: %s", err)
- return
- }
- return
-}
-
-type FSRSCard struct {
- *BaseCard
- C *fsrs.Card
-}
-
-func (card *FSRSCard) Impl() interface{} {
- return card.C
-}
-
-func (card *FSRSCard) SetImpl(c interface{}) {
- card.C = c.(*fsrs.Card)
-}
-
-func (card *FSRSCard) GetLapses() int {
- return int(card.C.Lapses)
-}
-
-func (card *FSRSCard) GetReps() int {
- return int(card.C.Reps)
-}
-
-func (card *FSRSCard) GetState() State {
- return State(card.C.State)
-}
-
-func (card *FSRSCard) GetLastReview() time.Time {
- return card.C.LastReview
-}
-
-func (card *FSRSCard) Clone() Card {
- data, err := gulu.JSON.MarshalJSON(card)
- if nil != err {
- logging.LogErrorf("marshal card failed: %s", err)
- return nil
- }
- ret := &FSRSCard{}
- if err = gulu.JSON.UnmarshalJSON(data, ret); nil != err {
- logging.LogErrorf("unmarshal card failed: %s", err)
- return nil
- }
- return ret
-}
-
-func (card *FSRSCard) SetDue(due time.Time) {
- card.C.Due = due
-}
diff --git a/fsrs_store_test.go b/fsrs_store_test.go
deleted file mode 100644
index 6edb3ae..0000000
--- a/fsrs_store_test.go
+++ /dev/null
@@ -1,121 +0,0 @@
-// Riff - Spaced repetition.
-// Copyright (c) 2022-present, b3log.org
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package riff
-
-import (
- "github.com/88250/gulu"
- "os"
- "strings"
- "testing"
- "time"
-
- "github.com/open-spaced-repetition/go-fsrs"
-)
-
-const (
- requestRetention = 0.9
- maximumInterval = 36500
- weights = "0.40, 0.60, 2.40, 5.80, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18, 0.05, 0.34, 1.26, 0.29, 2.61"
-)
-
-func TestFSRSStore(t *testing.T) {
- const storePath = "testdata"
- os.MkdirAll(storePath, 0755)
- defer os.RemoveAll(storePath)
-
- store := NewFSRSStore("test-store", storePath, requestRetention, maximumInterval, weights)
- p := fsrs.DefaultParam()
- start := time.Now()
- repeatTime := start
- ids := map[string]bool{}
- var firstCardID, firstBlockID, lastCardID, lastBlockID string
- max := 10000
- for i := 0; i < max; i++ {
- id, blockID := newID(), newID()
- if 0 == i {
- firstCardID = id
- firstBlockID = blockID
- } else if max-1 == i {
- lastCardID = id
- lastBlockID = blockID
- }
- store.AddCard(id, blockID)
- card := store.GetCard(id)
- c := *card.Impl().(*fsrs.Card)
- ids[id] = true
-
- for j := 0; j < 10; j++ {
- schedulingInfo := p.Repeat(c, repeatTime)
- c = schedulingInfo[fsrs.Hard].Card
- repeatTime = c.Due
- }
- repeatTime = start
- }
- cardsLen := len(store.cards)
- t.Logf("cards len [%d]", cardsLen)
- if len(ids) != len(store.cards) {
- t.Fatalf("cards len [%d] != ids len [%d]", len(store.cards), len(ids))
- }
-
- count := store.CountCards()
- if cardsLen != count {
- t.Fatalf("cards len [%d] != count [%d]", cardsLen, count)
- }
-
- if err := store.Save(); nil != err {
- t.Fatal(err)
- }
- t.Logf("saved cards [len=%d]", len(store.cards))
-
- if err := store.Load(); nil != err {
- t.Fatal(err)
- }
- t.Logf("loaded cards len [%d]", len(store.cards))
-
- if cardsLen != len(store.cards) {
- t.Fatal("cards len not equal")
- }
-
- cards := store.GetCardsByBlockID(firstBlockID)
- if 1 != len(cards) {
- t.Fatalf("cards by block id [len=%d]", len(cards))
- }
- if firstCardID != cards[0].ID() {
- t.Fatalf("cards by block id [cardID=%s]", cards[0].ID())
- }
-
- cards = store.GetCardsByBlockID(lastBlockID)
- if 1 != len(cards) {
- t.Fatalf("cards by block id [len=%d]", len(cards))
- }
- if lastCardID != cards[0].ID() {
- t.Fatalf("cards by block id [cardID=%s]", cards[0].ID())
- }
-
- cards = store.GetCardsByBlockIDs([]string{firstBlockID, lastBlockID})
- if 2 != len(cards) {
- t.Fatalf("cards by block ids [len=%d]", len(cards))
- }
- cardIDs := []string{cards[0].ID(), cards[1].ID()}
- if !gulu.Str.Contains(firstCardID, cardIDs) {
- t.Fatalf("cards by block ids [cardIDs=%v]", cardIDs)
- }
- if !gulu.Str.Contains(lastCardID, cardIDs) {
- t.Fatalf("cards by block ids [cardIDs=%v]", cardIDs)
- }
- t.Logf("cards by block ids [len=%d], card ids [%s]", len(cards), strings.Join(cardIDs, ", "))
-}
diff --git a/go.mod b/go.mod
index b0331b6..377ff7b 100644
--- a/go.mod
+++ b/go.mod
@@ -5,35 +5,44 @@ go 1.21
toolchain go1.21.6
require (
- github.com/88250/gulu v1.2.3-0.20231209020950-b7b6994e395c
+ github.com/88250/gulu v1.2.3-0.20240505150113-bc43bd50f866
+ github.com/mattn/go-sqlite3 v1.14.22
github.com/open-spaced-repetition/go-fsrs v1.2.1
- github.com/siyuan-note/filelock v0.0.0-20240419132904-2fbfe64f1939
- github.com/siyuan-note/logging v0.0.0-20231208035918-61f884c854f0
- github.com/vmihailenco/msgpack/v5 v5.4.1
+ github.com/siyuan-note/filelock v0.0.0-20240629145917-7545564cf0a4
+ github.com/siyuan-note/logging v0.0.0-20240505035402-6430d57006a2
+ github.com/syndtr/goleveldb v1.0.0
+ xorm.io/xorm v1.3.9
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
- github.com/cloudflare/circl v1.3.8 // indirect
- github.com/getsentry/sentry-go v0.27.0 // indirect
+ github.com/cloudflare/circl v1.3.9 // indirect
+ github.com/getsentry/sentry-go v0.28.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
- github.com/google/pprof v0.0.0-20240430035430-e4905b036c4e // indirect
+ github.com/goccy/go-json v0.9.11 // indirect
+ github.com/golang/snappy v0.0.4 // indirect
+ github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
- github.com/imroc/req/v3 v3.43.3 // indirect
- github.com/klauspost/compress v1.17.8 // indirect
- github.com/onsi/ginkgo/v2 v2.17.2 // indirect
+ github.com/imroc/req/v3 v3.43.7 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/compress v1.17.9 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/onsi/ginkgo/v2 v2.19.0 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
- github.com/quic-go/quic-go v0.43.0 // indirect
- github.com/refraction-networking/utls v1.6.4 // indirect
- github.com/siyuan-note/httpclient v0.0.0-20240429013218-3caa1f89f9ed // indirect
- github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ github.com/quic-go/quic-go v0.45.1 // indirect
+ github.com/refraction-networking/utls v1.6.6 // indirect
+ github.com/siyuan-note/httpclient v0.0.0-20240626145026-29585d45a51c // indirect
go.uber.org/mock v0.4.0 // indirect
- golang.org/x/crypto v0.22.0 // indirect
- golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
- golang.org/x/mod v0.17.0 // indirect
- golang.org/x/net v0.24.0 // indirect
- golang.org/x/sys v0.19.0 // indirect
- golang.org/x/text v0.14.0 // indirect
- golang.org/x/tools v0.20.0 // indirect
+ golang.org/x/crypto v0.24.0 // indirect
+ golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
+ golang.org/x/mod v0.18.0 // indirect
+ golang.org/x/net v0.26.0 // indirect
+ golang.org/x/sys v0.21.0 // indirect
+ golang.org/x/text v0.16.0 // indirect
+ golang.org/x/tools v0.22.0 // indirect
+ modernc.org/ccgo/v3 v3.17.0 // indirect
+ modernc.org/libc v1.52.1 // indirect
+ xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 // indirect
)
diff --git a/go.sum b/go.sum
index b728182..e87ef82 100644
--- a/go.sum
+++ b/go.sum
@@ -1,36 +1,74 @@
-github.com/88250/gulu v1.2.3-0.20231209020950-b7b6994e395c h1:Fas3hxqP33xA9KKDV50jUmppiiOukk5bdV00Hk5VSSk=
-github.com/88250/gulu v1.2.3-0.20231209020950-b7b6994e395c/go.mod h1:pTWnjt+6qUqNnP9xltswsJxgCBVu3C7eW09u48LWX0k=
+gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
+gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
+github.com/88250/gulu v1.2.3-0.20240505150113-bc43bd50f866 h1:RFfNFS0hv6TbOuwET6xZAfGlV4hNlXiWTnfbLN1eF6k=
+github.com/88250/gulu v1.2.3-0.20240505150113-bc43bd50f866/go.mod h1:MUfzyfmbPrRDZLqxc7aPrVYveatTHRfoUa5TynPS0i8=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
-github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
-github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
+github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
+github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
-github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k=
+github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
+github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/pprof v0.0.0-20240430035430-e4905b036c4e h1:RsXNnXE59RTt8o3DcA+w7ICdRfR2l+Bb5aE0YMpNTO8=
-github.com/google/pprof v0.0.0-20240430035430-e4905b036c4e/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 h1:e+8XbKB6IMn8A4OAyZccO4pYfB3s7bt6azNIPE7AnPg=
+github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/imroc/req/v3 v3.43.3 h1:WdZhpUev9THtuwEZsW2LOYacl12fm7IkB7OgACv40+k=
-github.com/imroc/req/v3 v3.43.3/go.mod h1:SQIz5iYop16MJxbo8ib+4LnostGCok8NQf8ToyQc2xA=
-github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
-github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
-github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g=
-github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
-github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
-github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/imroc/req/v3 v3.43.7 h1:dOcNb9n0X83N5/5/AOkiU+cLhzx8QFXjv5MhikazzQA=
+github.com/imroc/req/v3 v3.43.7/go.mod h1:SQIz5iYop16MJxbo8ib+4LnostGCok8NQf8ToyQc2xA=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
+github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/open-spaced-repetition/go-fsrs v1.2.1 h1:vY1hSQ3gvHtfnw8ahylcZyyqusKWDkWCd1+ca4lZoSc=
github.com/open-spaced-repetition/go-fsrs v1.2.1/go.mod h1:WpbNs4TTKZChOHFO+ME0B9femUVZsepFT5mhAioszRg=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
@@ -41,43 +79,79 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
-github.com/quic-go/quic-go v0.43.0 h1:sjtsTKWX0dsHpuMJvLxGqoQdtgJnbAPWY+W+5vjYW/g=
-github.com/quic-go/quic-go v0.43.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M=
-github.com/refraction-networking/utls v1.6.4 h1:aeynTroaYn7y+mFtqv8D0bQ4bw0y9nJHneGxJ7lvRDM=
-github.com/refraction-networking/utls v1.6.4/go.mod h1:2VL2xfiqgFAZtJKeUTlf+PSYFs3Eu7km0gCtXJ3m8zs=
-github.com/siyuan-note/filelock v0.0.0-20240419132904-2fbfe64f1939 h1:RQApoGXu6E5KX2Deb05c27bfJiA/Yzc34BKx+hRnCtM=
-github.com/siyuan-note/filelock v0.0.0-20240419132904-2fbfe64f1939/go.mod h1:0MqIa22SVvzvdI696uQAxvNmfOpD5gBh/i/wK4BECQo=
-github.com/siyuan-note/httpclient v0.0.0-20240429013218-3caa1f89f9ed h1:QBPxwSsWnAxxnEKYn3ycvFxj61vfpD8q4zTtLH9iN9w=
-github.com/siyuan-note/httpclient v0.0.0-20240429013218-3caa1f89f9ed/go.mod h1:qQsrqhPrCPJwjpHQetGcLCBh7bBCpoJ7P1uXoXJiClY=
-github.com/siyuan-note/logging v0.0.0-20231208035918-61f884c854f0 h1:+XjUr9UMXsczdO2bGA72p/k9wa2ShPb8ybi7CDBJ7HQ=
-github.com/siyuan-note/logging v0.0.0-20231208035918-61f884c854f0/go.mod h1:6mRFtAAvYPn3cDzqvyv+t8BVPGqpONDMMb5ywOhY1D4=
+github.com/quic-go/quic-go v0.45.1 h1:tPfeYCk+uZHjmDRwHHQmvHRYL2t44ROTujLeFVBmjCA=
+github.com/quic-go/quic-go v0.45.1/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
+github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig=
+github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/siyuan-note/filelock v0.0.0-20240629145917-7545564cf0a4 h1:Mk3712ngEHghxSqdyaCP2G+fI27kCTDYJLOSB22zb1c=
+github.com/siyuan-note/filelock v0.0.0-20240629145917-7545564cf0a4/go.mod h1:ftS1FyVHGGlyqXgGOVux4JLv8b2fXN9uJSpTRzCvjc8=
+github.com/siyuan-note/httpclient v0.0.0-20240626145026-29585d45a51c h1:E6W4x2GL+7jHsfkPEldvosWNl7hxtRHJjgGVhHHywn4=
+github.com/siyuan-note/httpclient v0.0.0-20240626145026-29585d45a51c/go.mod h1:RZ0DzDpOBiKMKOTMI2OdSNGBo7Fn6za6Q+ornWF0x+Q=
+github.com/siyuan-note/logging v0.0.0-20240505035402-6430d57006a2 h1:/2+tlOThVB86RxSLeW0JFw2ISUrH2ZFRg15ULGAUGAE=
+github.com/siyuan-note/logging v0.0.0-20240505035402-6430d57006a2/go.mod h1:3Osd2/nwzXZFl6ZcDE4hA0HD83Wyv1fds47nVuapyOM=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
-github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
-github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
-github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
+github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
-golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
-golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
-golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
-golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
+golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
+golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
+golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
+golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
+golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
+golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
-golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
-golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
+lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
+modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
+modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA=
+modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I=
+modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M=
+modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
+modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE=
+modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 h1:bvLlAPW1ZMTWA32LuZMBEGHAUOcATZjzHcotf3SWweM=
+xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
+xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
+xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
diff --git a/review_info.go b/review_info.go
new file mode 100644
index 0000000..69e212e
--- /dev/null
+++ b/review_info.go
@@ -0,0 +1,11 @@
+package riff
+
+type ReviewInfo struct {
+ BaseCard `xorm:"extends"`
+ BaseCardSource `xorm:"extends"`
+}
+
+func (ri *ReviewInfo) ToCard() (card Card) {
+ card = &ri.BaseCard
+ return
+}
diff --git a/riff.go b/riff.go
new file mode 100644
index 0000000..d2c0438
--- /dev/null
+++ b/riff.go
@@ -0,0 +1,822 @@
+package riff
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/fs"
+ "math/rand"
+ "os"
+ "path"
+ "path/filepath"
+ "reflect"
+ "sync"
+ "time"
+
+ "github.com/88250/gulu"
+ _ "github.com/mattn/go-sqlite3"
+ "github.com/open-spaced-repetition/go-fsrs"
+
+ "github.com/siyuan-note/filelock"
+ "github.com/siyuan-note/logging"
+ "github.com/syndtr/goleveldb/leveldb/errors"
+ "xorm.io/xorm"
+)
+
+type Riff interface {
+ Query() []map[string]interface{}
+ QueryCard() []Card
+
+ // 底层 xorm DB的 Find 加锁包装
+ Find(beans interface{}, condiBeans ...interface{}) error
+ SetParams(algo Algo, params interface{})
+ AddDeck(deck Deck) (newDeck Deck, err error)
+ AddCardSource(cardSources []CardSource) (cardSourceList []CardSource, err error)
+ AddCard(cards []Card) (cardList []Card, err error)
+ Load(savePath string) (err error)
+ //WaitLoad会等待至从磁盘加载完成
+ WaitLoad() (err error)
+ Save(path string) error
+ Due() []ReviewInfo
+ // Review(card Card, rating Rating, RequestRetention float64)
+ GetCardsByBlockIDs(blockIDs []string) (ret []ReviewInfo)
+ Review(cardID string, rating Rating)
+ CountCards() int
+ GetBlockIDs() (ret []string)
+
+ //SetDeck设置Deck, 无锁
+ SetDeck(deck Deck) (err error)
+
+ //GetDeck获取Deck, 无锁
+ GetDeck(did string) (deck Deck, err error)
+
+ //SetCardSource设置CardSource, 无锁
+ SetCardSource(cs CardSource) (err error)
+
+ //GetCardSource获取CardSource, 无锁
+ GetCardSource(csid string) (cs CardSource, err error)
+
+ //SetCard设置Card, 无锁
+ SetCard(card Card) (err error)
+
+ //GetCard获取Card, 无锁
+ GetCard(cardID string) (card Card, err error)
+}
+
+type BaseRiff struct {
+ db *xorm.Engine
+ GlobalRequestRetention float64
+ MaxRequestRetention float64
+ MinRequestRetention float64
+ lock *sync.Mutex
+ load *sync.Mutex
+ startTime time.Time
+ ParamsMap map[Algo]interface{}
+ deckMap map[string]Deck
+ cardSourceMap map[string]CardSource
+ cardMap map[string]Card
+}
+
+func NewBaseRiff() Riff {
+ // orm, err := xorm.NewEngine("sqlite", ":memory:?_pragma=foreign_keys(1)")
+ orm, err := xorm.NewEngine("sqlite3", ":memory:?mode=memory&cache=shared&loc=auto")
+ orm.Exec(`
+ create view block_id_to_card_source as
+ select c_s_i_d,value
+ from
+ base_card_source , json_each(base_card_source.block_i_ds)`)
+ if err != nil {
+ return &BaseRiff{}
+ }
+ orm.Sync(new(BaseCard), new(BaseCardSource), new(BaseDeck), new(BaseHistory), new(ReviewLog))
+ riff := BaseRiff{
+ db: orm,
+ GlobalRequestRetention: 0.900,
+ MaxRequestRetention: 0.999,
+ MinRequestRetention: 0.500,
+ lock: &sync.Mutex{},
+ load: &sync.Mutex{},
+ startTime: time.Now(),
+ ParamsMap: map[Algo]interface{}{},
+ deckMap: map[string]Deck{},
+ cardSourceMap: map[string]CardSource{},
+ cardMap: map[string]Card{},
+ }
+ return &riff
+}
+
+func (br *BaseRiff) SetParams(algo Algo, params interface{}) {
+ br.ParamsMap[algo] = params
+}
+
+func (br *BaseRiff) Query() []map[string]interface{} {
+ // 空实现
+ return nil
+}
+
+func (br *BaseRiff) QueryCard() []Card {
+ // 空实现
+ return nil
+}
+
+func (br *BaseRiff) AddDeck(deck Deck) (newDeck Deck, err error) {
+ br.lock.Lock()
+ defer br.lock.Unlock()
+ br.deckMap[deck.GetDID()] = deck
+ _, err = br.db.Insert(deck)
+ newDeck = deck
+ return
+}
+
+func (br *BaseRiff) batchCheck(table, field string, IDs []string) (existMap map[string]bool, err error) {
+
+ br.lock.Lock()
+ defer br.lock.Unlock()
+
+ const MAX_BATCH = 5000
+
+ existMap = map[string]bool{}
+ existsIDs := make([]string, 0)
+ IDs = gulu.Str.RemoveDuplicatedElem(IDs)
+ IDsLength := len(IDs)
+
+ for i := 0; i < IDsLength; i += MAX_BATCH {
+ end := i + MAX_BATCH
+ if end > IDsLength {
+ end = IDsLength
+ }
+ subIDs := IDs[i:end]
+ err = br.db.Table(table).
+ In(field, subIDs).
+ Cols(field).
+ Find(&existsIDs)
+ }
+
+ for _, existsID := range existsIDs {
+ existMap[existsID] = true
+ }
+ for _, ID := range IDs {
+ if !existMap[ID] {
+ err = errors.New(fmt.Sprintf("no exit field in %s : %s = %s", table, field, ID))
+ return
+ }
+ }
+
+ return
+}
+
+func (br *BaseRiff) batchInsert(rowSlice interface{}) (err error) {
+ // 获取数据类型
+
+ br.lock.Lock()
+ defer br.lock.Unlock()
+
+ t := reflect.TypeOf(rowSlice)
+
+ // 检查是否是切片类型
+ if t.Kind() != reflect.Slice {
+ fmt.Println("Not a slice")
+ return
+ }
+ sliceValue := reflect.Indirect(reflect.ValueOf(rowSlice))
+ Len := sliceValue.Len()
+ session := br.db.NewSession()
+ defer session.Close()
+ session.Begin()
+ for i := 0; i < Len; i++ {
+ _, err = session.Insert(sliceValue.Index(i).Interface())
+ if err != nil {
+ fmt.Printf("error on insert CardSource %s \n", err)
+ continue
+ }
+ }
+ err = session.Commit()
+ return
+}
+
+func (br *BaseRiff) AddCardSource(cardSources []CardSource) (cardSourceList []CardSource, err error) {
+
+ DIDs := make([]string, 0)
+ existsCardSourceList := make([]CardSource, 0)
+ for index := range cardSources {
+ DIDs = append(DIDs, cardSources[index].GetDIDs()...)
+ }
+
+ existCSIDMap, err := br.batchCheck(
+ "base_deck",
+ "d_i_d",
+ DIDs,
+ )
+
+ for _, cardSource := range cardSources {
+ DIDs := cardSource.GetDIDs()
+ unExist := 0
+ for _, DID := range DIDs {
+ if !existCSIDMap[DID] {
+ unExist += 1
+ }
+ }
+ if unExist == 0 {
+ existsCardSourceList = append(existsCardSourceList, cardSource)
+ // 添加到 cardSourceMap
+ br.cardSourceMap[cardSource.GetCSID()] = cardSource
+ }
+ }
+
+ br.batchInsert(existsCardSourceList)
+
+ return
+}
+
+func (br *BaseRiff) AddCard(cards []Card) (cardList []Card, err error) {
+ // 空实现
+ // start := time.Now()
+
+ CSIDs := make([]string, 0)
+ existsCardList := make([]Card, 0)
+ for index := range cards {
+ cards[index].MarshalImpl()
+ CSIDs = append(CSIDs, cards[index].GetCSID())
+ }
+
+ existCSIDMap, err := br.batchCheck(
+ "base_card_source",
+ "c_s_i_d",
+ CSIDs,
+ )
+
+ for _, card := range cards {
+ if existCSIDMap[card.GetCSID()] {
+ br.cardMap[card.ID()] = card
+ existsCardList = append(existsCardList, card)
+ }
+ }
+
+ br.batchInsert(existsCardList)
+
+ return
+}
+
+func saveData(data interface{}, suffix SaveExt, saveDirPath string) (err error) {
+ byteData, err := json.Marshal(data)
+ if err != nil {
+ logging.LogErrorf("marshal logs failed: %s", err)
+ return
+ }
+ savePath := path.Join(saveDirPath, "siyuan"+string(suffix))
+ err = filelock.WriteFile(savePath, byteData)
+ if err != nil {
+ logging.LogErrorf("write riff file failed: %s", err)
+ return
+ }
+ return
+}
+
+func (br *BaseRiff) Find(beans interface{}, condiBeans ...interface{}) error {
+ br.lock.Lock()
+ defer br.lock.Unlock()
+ err := br.db.Find(beans, condiBeans...)
+ return err
+}
+
+func (br *BaseRiff) Get(beans ...interface{}) error {
+ br.lock.Lock()
+ defer br.lock.Unlock()
+ _, err := br.db.Get(beans...)
+ return err
+}
+
+func (br *BaseRiff) Save(path string) (err error) {
+ decks := make([]BaseDeck, 0)
+ cardSources := make([]BaseCardSource, 0)
+ cards := make([]BaseCard, 0)
+
+ for _, deck := range br.deckMap {
+ decks = append(decks, *(deck.(*BaseDeck)))
+ }
+ for _, cardSource := range br.cardSourceMap {
+ cardSources = append(cardSources, *(cardSource.(*BaseCardSource)))
+ }
+ for _, card := range br.cardMap {
+ cards = append(cards, *(card.(*BaseCard)))
+ }
+
+ if !gulu.File.IsDir(path) {
+ if err = os.MkdirAll(path, 0755); nil != err {
+ return
+ }
+ }
+ err = saveData(decks, DeckExt, path)
+ if err != nil {
+ fmt.Printf("err in save riff data: %s \n", err)
+ }
+ err = saveData(cardSources, CardSourceExt, path)
+ if err != nil {
+ fmt.Printf("err in save riff data: %s \n", err)
+ }
+ err = saveData(cards, CardExt, path)
+ if err != nil {
+ fmt.Printf("err in save riff data: %s \n", err)
+ }
+
+ err = br.SaveHistory(path)
+
+ return
+}
+
+func saveHistoryData(data interface{}, suffix SaveExt, saveDirPath string, time time.Time) (err error) {
+ byteData, err := json.Marshal(data)
+ if err != nil {
+ logging.LogErrorf("marshal logs failed: %s", err)
+ return
+ }
+ yyyyMMddHHmmss := time.Format("2006-01-02-15_04_05")
+ savePath := path.Join(saveDirPath, yyyyMMddHHmmss+string(suffix))
+ err = filelock.WriteFile(savePath, byteData)
+ if err != nil {
+ return
+ }
+ return
+}
+
+func (br *BaseRiff) SaveHistory(path string) (err error) {
+ historys := make([]BaseHistory, 0)
+ reviewLogs := make([]ReviewLog, 0)
+ err = br.Find(&historys)
+ if err != nil {
+ return
+ }
+
+ for i := range historys {
+ historys[i].UnmarshalImpl()
+ }
+
+ err = br.Find(&reviewLogs)
+ if err != nil {
+ return
+ }
+ err = saveHistoryData(historys, HistoryExt, path, br.startTime)
+ if err != nil {
+ logging.LogErrorf("write history file failed: %s", err)
+ }
+ err = saveHistoryData(reviewLogs, reviewLogExt, path, br.startTime)
+ if err != nil {
+ logging.LogErrorf("write log file failed: %s", err)
+ }
+
+ if err != nil {
+ return
+ }
+ return
+}
+
+func (br *BaseRiff) LoadHistory(savePath string) (err error) {
+ if !gulu.File.IsDir(savePath) {
+ return errors.New("no a save path")
+ }
+
+ totalHistory := make([]History, 0)
+ totalReviewLog := make([]ReviewLog, 0)
+
+ filelock.Walk(savePath, func(walkPath string, info fs.FileInfo, err error) (reErr error) {
+ if info.IsDir() {
+ return
+ }
+ ext := filepath.Ext(walkPath)
+ data, reErr := filelock.ReadFile(walkPath)
+ switch SaveExt(ext) {
+
+ case HistoryExt:
+ historys := make([]BaseHistory, 0)
+ json.Unmarshal(data, &historys)
+ for _, history := range historys {
+ copy := history
+ totalHistory = append(totalHistory, ©)
+ }
+ case reviewLogExt:
+ reviewLog := make([]ReviewLog, 0)
+ json.Unmarshal(data, &reviewLog)
+ totalReviewLog = append(totalReviewLog, reviewLog...)
+ }
+
+ return
+ })
+ br.batchInsert(totalHistory)
+ br.batchInsert(totalReviewLog)
+ return
+}
+
+func (br *BaseRiff) Load(savePath string) (err error) {
+ br.load.Lock()
+ defer br.load.Unlock()
+
+ if !gulu.File.IsDir(savePath) {
+ return errors.New("no a save path")
+ }
+ totalDecks := make([]Deck, 0)
+ totalCards := make([]Card, 0)
+ totalCardSources := make([]CardSource, 0)
+
+ filelock.Walk(savePath, func(walkPath string, info fs.FileInfo, err error) (reErr error) {
+ if info.IsDir() {
+ return
+ }
+ ext := filepath.Ext(walkPath)
+ // 后期性能改进点:把读文件位置后移
+ data, reErr := filelock.ReadFile(walkPath)
+ switch SaveExt(ext) {
+
+ case DeckExt:
+ decks := make([]BaseDeck, 0)
+ json.Unmarshal(data, &decks)
+ for _, deck := range decks {
+ copy := deck
+ totalDecks = append(totalDecks, ©)
+ }
+
+ case CardExt:
+ cards := make([]BaseCard, 0)
+ json.Unmarshal(data, &cards)
+ for _, card := range cards {
+ copy := card
+ totalCards = append(totalCards, ©)
+ }
+
+ case CardSourceExt:
+ cardSources := make([]BaseCardSource, 0)
+ json.Unmarshal(data, &cardSources)
+ for _, cardSource := range cardSources {
+ copy := cardSource
+ totalCardSources = append(totalCardSources, ©)
+ }
+ }
+
+ return
+ })
+ for _, deck := range totalDecks {
+ br.AddDeck(deck)
+ }
+
+ br.AddCardSource(totalCardSources)
+ br.AddCard(totalCards)
+
+ go br.LoadHistory(savePath)
+ return
+
+}
+
+func (br *BaseRiff) WaitLoad() (err error) {
+ br.load.Lock()
+ defer br.load.Unlock()
+ return
+}
+
+func (br *BaseRiff) Due() (ret []ReviewInfo) {
+ now := time.Now()
+
+ qr := br.newReviewInfoQuery()
+ qr, err := qr.ByDue(now)
+ if err != nil {
+ return
+ }
+ ret, err = qr.Query()
+ if err != nil {
+ ret = make([]ReviewInfo, 0)
+ return
+ }
+ return
+}
+
+func (br *BaseRiff) GetCardsByBlockIDs(blockIDs []string) (ret []ReviewInfo) {
+
+ qr := br.newReviewInfoQuery()
+ qr, err := qr.ByBlockIDs(blockIDs)
+
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ ret, err = qr.Query()
+
+ if err != nil {
+ fmt.Println(err)
+ }
+
+ return
+}
+
+func (br *BaseRiff) innerReview(card Card, rating Rating, RequestRetention float64) {
+ br.lock.Lock()
+ defer br.lock.Unlock()
+ now := time.Now()
+
+ history := NewBaseHistory(card)
+ reviewlog := NewReviewLog(history, rating)
+ _, err := br.db.Insert(history)
+ if err != nil {
+ logging.LogErrorf("error insert history %s \n", err)
+ }
+ _, err = br.db.Insert(reviewlog)
+ if err != nil {
+ logging.LogErrorf("error insert reviewLog %s \n", err)
+ }
+
+ switch card.GetAlgo() {
+ case AlgoFSRS:
+ fsrsCard := (card.Impl()).(fsrs.Card)
+ params := br.ParamsMap[AlgoFSRS].(fsrs.Parameters)
+ params.RequestRetention = RequestRetention
+ schedulingInfo := params.Repeat(fsrsCard, now)[rating.ToFsrs()]
+ newCard := schedulingInfo.Card
+ card.SetDue(newCard.Due)
+ card.SetImpl(newCard)
+ }
+
+ RatingMap := map[Rating]State{}
+ preState := card.GetState()
+ switch preState {
+ case New:
+ RatingMap[Again] = Learning
+ RatingMap[Hard] = Learning
+ RatingMap[Good] = Learning
+ RatingMap[Easy] = Review
+
+ case Learning, Relearning:
+ RatingMap[Again] = preState
+ RatingMap[Hard] = preState
+ RatingMap[Good] = Review
+ RatingMap[Easy] = Review
+ case Review:
+ RatingMap[Again] = Relearning
+ RatingMap[Hard] = Review
+ RatingMap[Good] = Review
+ RatingMap[Easy] = Review
+ }
+ currentState := RatingMap[rating]
+ card.SetState(currentState)
+ card.SetReps(card.GetReps() + 1)
+ if currentState == Relearning {
+ card.SetLapses(card.GetLapses() + 1)
+ }
+
+ br.SetCard(card)
+}
+
+func (br *BaseRiff) Review(cardID string, rating Rating) {
+ br.lock.Lock()
+ card, err := br.GetCard(cardID)
+ br.lock.Unlock()
+ if err != nil {
+ return
+ }
+
+ RequestRetention := br.getRequestRetention((card))
+ br.innerReview(card, rating, RequestRetention)
+
+}
+func (br *BaseRiff) getRequestRetention(card Card) float64 {
+ priority := card.GetPriority()
+ requestRetention := br.GlobalRequestRetention
+ switch {
+ case priority >= 0 && priority <= 0.5:
+ requestRetention = (br.GlobalRequestRetention - br.MinRequestRetention) / (0.5 - 0) * (priority - 0)
+ case priority > 0.5 && priority <= 1:
+ requestRetention = (br.MaxRequestRetention - br.GlobalRequestRetention) / (1 - 0.5) * (priority - 0.5)
+ }
+ return requestRetention
+}
+
+func (br *BaseRiff) CountCards() int {
+ // 空实现
+ return 0
+}
+
+func (br *BaseRiff) GetBlockIDs() (ret []string) {
+ // 空实现
+ return nil
+}
+
+func (br *BaseRiff) SetDeck(deck Deck) (err error) {
+ br.deckMap[deck.GetDID()] = deck
+ _, err = br.db.Where("d_i_d = ?", deck.GetDID()).Update(deck)
+ return
+}
+
+func (br *BaseRiff) GetDeck(did string) (deck Deck, err error) {
+ deck, exist := br.deckMap[did]
+ if !exist {
+ err = fmt.Errorf("deck %s is not exist", did)
+ }
+ return
+}
+
+func (br *BaseRiff) SetCardSource(cs CardSource) (err error) {
+ br.cardSourceMap[cs.GetCSID()] = cs
+ _, err = br.db.Where("c_s_i_d = ?", cs.GetCSID()).Update(cs)
+ return
+}
+
+func (br *BaseRiff) GetCardSource(csid string) (cs CardSource, err error) {
+ cs, exist := br.cardSourceMap[csid]
+ if !exist {
+ err = fmt.Errorf("CardSource %s is not exist", csid)
+ }
+ return
+}
+
+func (br *BaseRiff) SetCard(card Card) (err error) {
+ br.cardMap[card.ID()] = card
+ card.MarshalImpl()
+ _, err = br.db.Where("c_i_d = ?", card.ID()).Update(card)
+ if err != nil {
+ fmt.Printf("update card err:%s\n", err)
+ }
+
+ return
+}
+
+func (br *BaseRiff) GetCard(cardID string) (card Card, err error) {
+ card, exist := br.cardMap[cardID]
+ if !exist {
+ err = fmt.Errorf("card %s is not exist", cardID)
+ }
+ return
+}
+
+// QueryReviewInfo 是对 ReviewInfo 搜索查询操作的抽象和封装
+type QueryReviewInfo interface {
+ ByBlockIDs(blockIDs []string) (ret QueryReviewInfo, err error)
+ ByDue(dueTime time.Time) (ret QueryReviewInfo, err error)
+ Query() (ret []ReviewInfo, err error)
+ Conut() (ret int64, err error)
+}
+type baseQueryReviewInfo struct {
+ br *BaseRiff
+ db *xorm.Engine
+ sission *xorm.Session
+ lock *sync.Mutex
+}
+
+func (br *BaseRiff) newReviewInfoQuery() (ret QueryReviewInfo) {
+ br.lock.Lock()
+ defer br.lock.Unlock()
+ session := br.db.Table("base_card").
+ Join("inner", "base_card_source", "base_card.c_s_i_d = base_card_source.c_s_i_d")
+ ret = &baseQueryReviewInfo{
+ br: br,
+ db: br.db,
+ sission: session,
+ lock: br.lock,
+ }
+ return
+}
+
+func (qr *baseQueryReviewInfo) ByBlockIDs(blockIDs []string) (ret QueryReviewInfo, err error) {
+ qr.lock.Lock()
+ defer qr.lock.Unlock()
+
+ ret = qr
+
+ existSet := map[string]bool{}
+ csidMap := map[string]bool{}
+ csidList := make([]string, 0)
+
+ for _, blockID := range blockIDs {
+ existSet[blockID] = true
+ }
+
+ for _, cs := range qr.br.cardSourceMap {
+ csBlockIDs := cs.GetBlockIDs()
+ csid := cs.GetCSID()
+ for _, csBlockID := range csBlockIDs {
+ if existSet[csBlockID] {
+ csidMap[csid] = true
+ }
+ }
+ }
+
+ for csid := range csidMap {
+ csidList = append(csidList, csid)
+ }
+
+ queryCsidLen := len(csidList)
+ if queryCsidLen > MAX_QUERY_PARAMS {
+ queryCsidLen = MAX_QUERY_PARAMS
+ }
+ queryCsidList := csidList[0:queryCsidLen]
+
+ qr.sission.In("base_card_source.c_s_i_d", queryCsidList)
+ return
+}
+
+func (qr *baseQueryReviewInfo) ByDue(dueTime time.Time) (ret QueryReviewInfo, err error) {
+ qr.lock.Lock()
+ defer qr.lock.Unlock()
+
+ ret = qr
+
+ qr.sission.Where("base_card.due < ?", dueTime)
+ return
+}
+
+func (qr *baseQueryReviewInfo) Query() (ret []ReviewInfo, err error) {
+ qr.lock.Lock()
+ defer qr.lock.Unlock()
+ ret = make([]ReviewInfo, 0)
+ reviewInfoIDList, err := qr.sission.
+ Cols("base_card.c_i_d", "base_card_source.c_s_i_d").
+ QueryString()
+ if err != nil {
+ return
+ }
+ for _, item := range reviewInfoIDList {
+ card, err := qr.br.GetCard(item["c_i_d"])
+ if err != nil {
+ continue
+ }
+ cs, err := qr.br.GetCardSource(item["c_s_i_d"])
+ if err != nil {
+ continue
+ }
+ ret = append(ret, ReviewInfo{
+ BaseCard: *(card.(*BaseCard)),
+ BaseCardSource: *(cs.(*BaseCardSource)),
+ })
+ }
+
+ return
+}
+
+func (qr *baseQueryReviewInfo) Conut() (ret int64, err error) {
+ qr.lock.Lock()
+ defer qr.lock.Unlock()
+ ri := new(ReviewInfo)
+ ret, err = qr.sission.Count(ri)
+ return
+}
+
+// Rating 描述了闪卡复习的评分。
+type Rating int8
+
+const (
+ Again Rating = iota + 1 // 完全不会,必须再复习一遍
+ Hard // 有点难
+ Good // 一般
+ Easy // 很容易
+)
+
+var RatingToFsrs = map[Rating]fsrs.Rating{
+ Again: fsrs.Again,
+ Hard: fsrs.Hard,
+ Good: fsrs.Good,
+ Easy: fsrs.Easy,
+}
+
+func (rate Rating) ToFsrs() fsrs.Rating {
+ return RatingToFsrs[rate]
+}
+
+// Algo 描述了闪卡复习算法的名称。
+type Algo string
+
+const (
+ AlgoFSRS Algo = "fsrs"
+ AlgoSM2 Algo = "sm2"
+)
+
+type SaveExt string
+
+const (
+ CardExt = ".cards"
+ CardSourceExt = ".cardSources"
+ DeckExt = ".decks"
+ HistoryExt = ".history"
+ reviewLogExt = ".revlog"
+)
+
+// State 描述了闪卡的状态。
+type State int8
+
+const (
+ New State = iota
+ Learning
+ Review
+ Relearning
+)
+
+const builtInDeck = "20240718214745-q7ocvvi"
+
+const MAX_QUERY_PARAMS = 3_0000
+
+func newID() string {
+ now := time.Now()
+ return now.Format("20060102150405") + "-" + randStr(7)
+}
+
+func randStr(length int) string {
+ letter := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
+ b := make([]rune, length)
+ for i := range b {
+ b[i] = letter[rand.Intn(len(letter))]
+ }
+ return string(b)
+}
diff --git a/riff_test.go b/riff_test.go
new file mode 100644
index 0000000..a1c7372
--- /dev/null
+++ b/riff_test.go
@@ -0,0 +1,433 @@
+// Riff - Spaced repetition.
+// Copyright (c) 2022-present, b3log.org
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package riff
+
+import (
+ "fmt"
+ "math/rand"
+ "os"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/open-spaced-repetition/go-fsrs"
+)
+
+type timeTicker struct {
+ totalTicker map[string]time.Time
+}
+
+func newTimeTicker() *timeTicker {
+ return &timeTicker{
+ totalTicker: map[string]time.Time{},
+ }
+}
+
+func (tt *timeTicker) start(task string) {
+ tt.totalTicker[task] = time.Now()
+}
+func (tt *timeTicker) log(task string) {
+ since := time.Since(tt.totalTicker[task])
+ fmt.Printf("task %s use time :%s\n", task, since)
+}
+
+func checkIDList(IDList []string, data interface{}, dataGetter func(item interface{}) string) (err error) {
+ queryIDs := map[string]bool{}
+ v := reflect.ValueOf(data)
+
+ for i := 0; i < v.Len(); i++ {
+ id := dataGetter(v.Index(i).Interface())
+ queryIDs[id] = true
+ }
+ for _, ID := range IDList {
+ if !queryIDs[ID] {
+ err = fmt.Errorf("dont have id of %s", ID)
+ return
+ }
+ }
+ return
+}
+
+func randomSubset[T any](slice []T, size int) ([]T, error) {
+ if size > len(slice) {
+ return nil, fmt.Errorf("requested subset size is larger than the slice size")
+ }
+
+ // Create a copy of the original slice to avoid modifying it
+
+ subset := make([]T, size)
+ // copy(subset, slice)
+ sourceLen := len(slice)
+
+ for i := range subset {
+ subset[i] = slice[rand.Intn(sourceLen)]
+ }
+
+ // Return the first `size` elements
+ return subset[:size], nil
+}
+
+// 检查功能是否正确实现
+func TestFunction(t *testing.T) {
+ const saveDir = "testdata"
+ const RequestRetention = 0.95
+ const cardSourceNum = 200
+ const blocksNum = 40
+ const preSourceCardNum = 3
+ const totalCardNum = cardSourceNum * preSourceCardNum
+
+ os.MkdirAll(saveDir, 0755)
+ defer os.RemoveAll(saveDir)
+
+ blocksIDs := []string{}
+ blockIDsCIDMap := make(map[string][]string, 0)
+
+ for i := 0; i < blocksNum; i++ {
+ blocksIDs = append(blocksIDs, newID())
+ }
+
+ riff := NewBaseRiff()
+ riff.SetParams(AlgoFSRS, fsrs.DefaultParam())
+ deck := DefaultBaseDeck()
+ csList := []CardSource{}
+ cardList := []Card{}
+ csIDList := []string{}
+ cardIDList := []string{}
+
+ for i := 0; i < cardSourceNum; i++ {
+ cs := NewBaseCardSource(deck.DID)
+
+ // 放入blockIDs
+ subBlockIDs, _ := randomSubset(blocksIDs, preSourceCardNum)
+ cs.BlockIDs = subBlockIDs
+ // 准备一个临时数组来储存当前blocksIDs对应的cardSourceID
+ for _, blockID := range subBlockIDs {
+ blockIDsCIDMap[blockID] = append(blockIDsCIDMap[blockID], cs.CSID)
+ }
+
+ csList = append(csList, cs)
+ csIDList = append(csIDList, cs.CSID)
+ for i := 0; i < preSourceCardNum; i++ {
+ card := NewBaseCard(cs)
+ card.UseAlgo(AlgoFSRS)
+ cardList = append(cardList, card)
+ cardIDList = append(cardIDList, card.CID)
+ }
+ }
+
+ //检查待插入卡片数量
+ if len(csIDList) != cardSourceNum {
+ t.Errorf("add card source error")
+ }
+
+ if len(cardIDList) != totalCardNum {
+ t.Errorf("add card error")
+ }
+
+ riff.AddDeck(deck)
+ riff.AddCardSource(csList)
+ riff.AddCard(cardList)
+
+ queryCsList := []BaseCardSource{}
+ queryCardList := []BaseCard{}
+
+ // 确保cardsource完全插入数据库
+ riff.Find(&queryCsList)
+ if len(queryCsList) != cardSourceNum {
+ t.Errorf("add CardSource err num %d:%d ", len(queryCsList), cardSourceNum)
+ }
+ if err := checkIDList(csIDList, queryCsList, func(item interface{}) string {
+ return item.(BaseCardSource).CSID
+ }); err != nil {
+ t.Errorf("%s", err)
+ }
+
+ // 确保card完全插入数据库
+ riff.Find(&queryCardList)
+ if len(queryCardList) != totalCardNum {
+ t.Errorf("add Card err num %d:%d ", len(queryCardList), totalCardNum)
+ }
+
+ if err := checkIDList(cardIDList, queryCardList, func(item interface{}) string {
+ return item.(BaseCard).CID
+ }); err != nil {
+ t.Errorf("%s", err)
+ }
+
+ reviewInfoList := riff.Due()
+ for _, reviewInfo := range reviewInfoList {
+ riff.Review(reviewInfo.BaseCard.ID(), Again)
+ }
+
+ reviewInfoList = riff.Due()
+ for _, reviewInfo := range reviewInfoList {
+ riff.Review(reviewInfo.BaseCard.ID(), Easy)
+ }
+
+ newreviewCard := riff.Due()
+
+ if len(newreviewCard) != 0 {
+ t.Errorf("review error with un review card num :%d\n", len(newreviewCard))
+ }
+
+ // 抽取一半的blockIDs检查
+ testBlockIDs, _ := randomSubset(blocksIDs, blocksNum/2)
+ reviewInfoByblocks := riff.GetCardsByBlockIDs(testBlockIDs)
+ existMap := map[string]bool{}
+ for _, ri := range reviewInfoByblocks {
+ existMap[ri.BaseCardSource.CSID] = true
+ }
+ for _, blockID := range testBlockIDs {
+ mapCSIDs := blockIDsCIDMap[blockID]
+ for _, csid := range mapCSIDs {
+ if !existMap[csid] {
+ t.Errorf("blockID %s cardSource %s no call back", blockID, csid)
+ }
+ }
+ }
+
+ riff.Save(saveDir)
+
+ newRiff := NewBaseRiff()
+ newRiff.SetParams(AlgoFSRS, fsrs.DefaultParam())
+ newRiff.Load(saveDir)
+
+ // 检查重新加载后 数据是否恢复
+
+ queryCsList = []BaseCardSource{}
+ queryCardList = []BaseCard{}
+
+ // 确保cardsource完全插入数据库
+ newRiff.Find(&queryCsList)
+ if len(queryCsList) != cardSourceNum {
+ t.Errorf("add CardSource err num %d:%d ", len(queryCsList), cardSourceNum)
+ }
+ if err := checkIDList(csIDList, queryCsList, func(item interface{}) string {
+ return item.(BaseCardSource).CSID
+ }); err != nil {
+ t.Errorf("%s", err)
+ }
+
+ // 确保card完全插入数据库
+ newRiff.Find(&queryCardList)
+ if len(queryCardList) != totalCardNum {
+ t.Errorf("add Card err num %d:%d ", len(queryCardList), totalCardNum)
+ }
+
+ if err := checkIDList(cardIDList, queryCardList, func(item interface{}) string {
+ return item.(BaseCard).CID
+ }); err != nil {
+ t.Errorf("%s", err)
+ }
+
+}
+
+// 检查性能参数
+// 10000 cardSOurce,
+// 30 card
+func TestPerformance(t *testing.T) {
+ ticker := newTimeTicker()
+ ticker.start("TestPerformance")
+
+ const saveDir = "testdata"
+ const RequestRetention = 0.95
+ const cardSourceNum = 20000
+ const blocksNum = 20000
+ const preSourceCardNum = 5
+ const totalCardNum = cardSourceNum * preSourceCardNum
+
+ os.MkdirAll(saveDir, 0755)
+ defer os.RemoveAll(saveDir)
+
+ blocksIDs := []string{}
+ blockIDsCIDMap := make(map[string][]string, 0)
+
+ for i := 0; i < blocksNum; i++ {
+ blocksIDs = append(blocksIDs, newID())
+ }
+
+ riff := NewBaseRiff()
+ riff.SetParams(AlgoFSRS, fsrs.DefaultParam())
+ deck := DefaultBaseDeck()
+
+ csList := []CardSource{}
+ cardList := []Card{}
+ csIDList := []string{}
+ cardIDList := []string{}
+ ticker.start("init card and cardsource")
+ for i := 0; i < cardSourceNum; i++ {
+ cs := NewBaseCardSource(deck.DID)
+
+ // 放入blockIDs
+ subBlockIDs, _ := randomSubset(blocksIDs, preSourceCardNum)
+ cs.BlockIDs = subBlockIDs
+ // 准备一个临时数组来储存当前blocksIDs对应的cardSourceID
+ for _, blockID := range subBlockIDs {
+ blockIDsCIDMap[blockID] = append(blockIDsCIDMap[blockID], cs.CSID)
+ }
+
+ csList = append(csList, cs)
+ csIDList = append(csIDList, cs.CSID)
+
+ if i%10 == 0 {
+ time.Sleep(1 * time.Microsecond)
+ }
+ for i := 0; i < preSourceCardNum; i++ {
+ card := NewBaseCard(cs)
+ card.UseAlgo(AlgoFSRS)
+ cardList = append(cardList, card)
+ cardIDList = append(cardIDList, card.CID)
+
+ }
+ }
+ ticker.log("init card and cardsource")
+
+ ticker.start("adddeck")
+ riff.AddDeck(deck)
+ ticker.log("adddeck")
+
+ ticker.start("add cardSource")
+ riff.AddCardSource(csList)
+ ticker.log("add cardSource")
+
+ ticker.start("add card")
+ riff.AddCard(cardList)
+ ticker.log("add card")
+
+ queryCsList := []BaseCardSource{}
+ queryCardList := []BaseCard{}
+
+ // 确保cardsource完全插入数据库
+ riff.Find(&queryCsList)
+ if len(queryCsList) != cardSourceNum {
+ t.Errorf("add CardSource err num %d:%d ", len(queryCsList), cardSourceNum)
+ }
+ if err := checkIDList(csIDList, queryCsList, func(item interface{}) string {
+ return item.(BaseCardSource).CSID
+ }); err != nil {
+ t.Errorf("%s", err)
+ }
+
+ // 确保card完全插入数据库
+ riff.Find(&queryCardList)
+ if len(queryCardList) != totalCardNum {
+ t.Errorf("add Card err num %d:%d ", len(queryCardList), totalCardNum)
+ }
+
+ if err := checkIDList(cardIDList, queryCardList, func(item interface{}) string {
+ return item.(BaseCard).CID
+ }); err != nil {
+ t.Errorf("%s", err)
+ }
+
+ ticker.start("query due")
+ reviewInfoList := riff.Due()
+ ticker.log("query due")
+
+ fmt.Printf("due card len :%d \n", len(reviewInfoList))
+
+ ticker.start("review")
+ for _, reviewInfo := range reviewInfoList {
+ riff.Review(reviewInfo.BaseCard.ID(), Again)
+ }
+ ticker.log("review")
+
+ ticker.start("query due again")
+ reviewInfoList = riff.Due()
+ ticker.log("query due again")
+
+ ticker.start("review again")
+ for _, reviewInfo := range reviewInfoList {
+ riff.Review(reviewInfo.BaseCard.ID(), Easy)
+ }
+ ticker.log("review again")
+
+ ticker.start("query due after review all")
+ newreviewCard := riff.Due()
+ ticker.log("query due after review all")
+
+ if len(newreviewCard) != 0 {
+ t.Errorf("review error with un review card num :%d\n", len(newreviewCard))
+ }
+
+ // 抽取一半的blockIDs检查
+ ticker.start("get card by blocks")
+ testBlockIDs, _ := randomSubset(blocksIDs, blocksNum)
+ reviewInfoByblocks := riff.GetCardsByBlockIDs(testBlockIDs)
+ fmt.Printf("reviewInfoByblocks len :%d \n", len(reviewInfoByblocks))
+ existMap := map[string]bool{}
+ for _, ri := range reviewInfoByblocks {
+ existMap[ri.BaseCardSource.CSID] = true
+ }
+ for _, blockID := range testBlockIDs {
+ mapCSIDs := blockIDsCIDMap[blockID]
+ for _, csid := range mapCSIDs {
+ if !existMap[csid] {
+ t.Errorf("blockID %s cardSource %s no call back", blockID, csid)
+ }
+ }
+ }
+ ticker.log("get card by blocks")
+
+ ticker.start("save")
+ riff.Save(saveDir)
+ ticker.log("save")
+
+ ticker.start("load")
+ newRiff := NewBaseRiff()
+ newRiff.SetParams(AlgoFSRS, fsrs.DefaultParam())
+ go newRiff.Load(saveDir)
+
+ ticker.log("load")
+
+ // 检查重新加载后 数据是否恢复
+
+ queryCsList = []BaseCardSource{}
+ queryCardList = []BaseCard{}
+
+ // 确保cardsource完全插入数据库
+ ticker.start("query total cardSource")
+ newRiff.WaitLoad()
+ newRiff.Find(&queryCsList)
+ ticker.log("query total cardSource")
+
+ if len(queryCsList) != cardSourceNum {
+ t.Errorf("add CardSource err num %d:%d ", len(queryCsList), cardSourceNum)
+ }
+ if err := checkIDList(csIDList, queryCsList, func(item interface{}) string {
+ return item.(BaseCardSource).CSID
+ }); err != nil {
+ t.Errorf("%s", err)
+ }
+
+ // 确保card完全插入数据库
+ ticker.start("query total card")
+ newRiff.Find(&queryCardList)
+ ticker.log("query total card")
+
+ if len(queryCardList) != totalCardNum {
+ t.Errorf("add Card err num %d:%d ", len(queryCardList), totalCardNum)
+ }
+
+ if err := checkIDList(cardIDList, queryCardList, func(item interface{}) string {
+ return item.(BaseCard).CID
+ }); err != nil {
+ t.Errorf("%s", err)
+ }
+ ticker.log("TestPerformance")
+ time.Sleep(5 * time.Second)
+}
diff --git a/store.go b/store.go
index 5d9d06d..e435910 100644
--- a/store.go
+++ b/store.go
@@ -17,10 +17,8 @@
package riff
import (
- "math/rand"
"path/filepath"
"sync"
- "time"
)
// Store 描述了闪卡存储。
@@ -113,45 +111,3 @@ func (store *BaseStore) GetSaveDir() string {
func (store *BaseStore) getMsgPackPath() string {
return filepath.Join(store.saveDir, store.id+".cards")
}
-
-// Rating 描述了闪卡复习的评分。
-type Rating int8
-
-const (
- Again Rating = iota + 1 // 完全不会,必须再复习一遍
- Hard // 有点难
- Good // 一般
- Easy // 很容易
-)
-
-// Algo 描述了闪卡复习算法的名称。
-type Algo string
-
-const (
- AlgoFSRS Algo = "fsrs"
- AlgoSM2 Algo = "sm2"
-)
-
-// State 描述了闪卡的状态。
-type State int8
-
-const (
- New State = iota
- Learning
- Review
- Relearning
-)
-
-func newID() string {
- now := time.Now()
- return now.Format("20060102150405") + "-" + randStr(7)
-}
-
-func randStr(length int) string {
- letter := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
- b := make([]rune, length)
- for i := range b {
- b[i] = letter[rand.Intn(len(letter))]
- }
- return string(b)
-}