siyuan/kernel/model/transaction.go

1620 lines
47 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SiYuan - Refactor your thinking
// Copyright (c) 2020-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 <https://www.gnu.org/licenses/>.
package model
import (
"bytes"
"errors"
"fmt"
"path/filepath"
"runtime/debug"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/88250/gulu"
"github.com/88250/lute"
"github.com/88250/lute/ast"
"github.com/88250/lute/editor"
"github.com/88250/lute/lex"
"github.com/88250/lute/parse"
"github.com/siyuan-note/filelock"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/av"
"github.com/siyuan-note/siyuan/kernel/cache"
"github.com/siyuan-note/siyuan/kernel/filesys"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/task"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func IsMoveOutlineHeading(transactions *[]*Transaction) bool {
for _, tx := range *transactions {
for _, op := range tx.DoOperations {
if "moveOutlineHeading" == op.Action {
return true
}
}
}
return false
}
func FlushTxQueue() {
time.Sleep(time.Duration(50) * time.Millisecond)
for 0 < len(txQueue) || isFlushing {
time.Sleep(10 * time.Millisecond)
}
}
var (
txQueue = make(chan *Transaction, 7)
flushLock = sync.Mutex{}
isFlushing = false
)
func init() {
go flushQueue()
}
func flushQueue() {
for {
select {
case tx := <-txQueue:
flushTx(tx)
}
}
}
func flushTx(tx *Transaction) {
defer logging.Recover()
flushLock.Lock()
isFlushing = true
defer func() {
isFlushing = false
flushLock.Unlock()
}()
start := time.Now()
if txErr := performTx(tx); nil != txErr {
switch txErr.code {
case TxErrCodeBlockNotFound:
util.PushTxErr("Transaction failed", txErr.code, nil)
return
case TxErrCodeDataIsSyncing:
util.PushMsg(Conf.Language(222), 5000)
default:
txData, _ := gulu.JSON.MarshalJSON(tx)
logging.LogFatalf(logging.ExitCodeFatal, "transaction failed [%d]: %s\n tx [%s]", txErr.code, txErr.msg, txData)
}
}
elapsed := time.Now().Sub(start).Milliseconds()
if 0 < len(tx.DoOperations) {
if 2000 < elapsed {
logging.LogWarnf("op tx [%dms]", elapsed)
}
}
}
func PerformTransactions(transactions *[]*Transaction) {
for _, tx := range *transactions {
tx.m = &sync.Mutex{}
txQueue <- tx
}
return
}
const (
TxErrCodeBlockNotFound = 0
TxErrCodeDataIsSyncing = 1
TxErrCodeWriteTree = 2
TxErrWriteAttributeView = 3
)
type TxErr struct {
code int
msg string
id string
}
func performTx(tx *Transaction) (ret *TxErr) {
if 1 > len(tx.DoOperations) {
return
}
//os.MkdirAll("pprof", 0755)
//cpuProfile, _ := os.Create("pprof/cpu_profile_tx")
//pprof.StartCPUProfile(cpuProfile)
//defer pprof.StopCPUProfile()
var err error
if err = tx.begin(); err != nil {
if strings.Contains(err.Error(), "database is closed") {
return
}
logging.LogErrorf("begin tx failed: %s", err)
ret = &TxErr{msg: err.Error()}
return
}
defer func() {
if e := recover(); nil != e {
stack := debug.Stack()
msg := fmt.Sprintf("PANIC RECOVERED: %v\n\t%s\n", e, stack)
logging.LogErrorf(msg)
if 1 == tx.state.Load() {
tx.rollback()
return
}
}
}()
for _, op := range tx.DoOperations {
switch op.Action {
case "create":
ret = tx.doCreate(op)
case "update":
ret = tx.doUpdate(op)
case "insert":
ret = tx.doInsert(op)
case "delete":
ret = tx.doDelete(op)
case "move":
ret = tx.doMove(op)
case "moveOutlineHeading":
ret = tx.doMoveOutlineHeading(op)
case "append":
ret = tx.doAppend(op)
case "appendInsert":
ret = tx.doAppendInsert(op)
case "prependInsert":
ret = tx.doPrependInsert(op)
case "foldHeading":
ret = tx.doFoldHeading(op)
case "unfoldHeading":
ret = tx.doUnfoldHeading(op)
case "setAttrs":
ret = tx.doSetAttrs(op)
case "doUpdateUpdated":
ret = tx.doUpdateUpdated(op)
case "addFlashcards":
ret = tx.doAddFlashcards(op)
case "removeFlashcards":
ret = tx.doRemoveFlashcards(op)
case "setAttrViewName":
ret = tx.doSetAttrViewName(op)
case "setAttrViewFilters":
ret = tx.doSetAttrViewFilters(op)
case "setAttrViewSorts":
ret = tx.doSetAttrViewSorts(op)
case "setAttrViewPageSize":
ret = tx.doSetAttrViewPageSize(op)
case "setAttrViewColWidth":
ret = tx.doSetAttrViewColumnWidth(op)
case "setAttrViewColWrap":
ret = tx.doSetAttrViewColumnWrap(op)
case "setAttrViewColHidden":
ret = tx.doSetAttrViewColumnHidden(op)
case "setAttrViewColPin":
ret = tx.doSetAttrViewColumnPin(op)
case "setAttrViewColIcon":
ret = tx.doSetAttrViewColumnIcon(op)
case "insertAttrViewBlock":
ret = tx.doInsertAttrViewBlock(op)
case "removeAttrViewBlock":
ret = tx.doRemoveAttrViewBlock(op)
case "addAttrViewCol":
ret = tx.doAddAttrViewColumn(op)
case "updateAttrViewCol":
ret = tx.doUpdateAttrViewColumn(op)
case "removeAttrViewCol":
ret = tx.doRemoveAttrViewColumn(op)
case "sortAttrViewRow":
ret = tx.doSortAttrViewRow(op)
case "sortAttrViewCol":
ret = tx.doSortAttrViewColumn(op)
case "sortAttrViewKey":
ret = tx.doSortAttrViewKey(op)
case "updateAttrViewCell":
ret = tx.doUpdateAttrViewCell(op)
case "updateAttrViewColOptions":
ret = tx.doUpdateAttrViewColOptions(op)
case "removeAttrViewColOption":
ret = tx.doRemoveAttrViewColOption(op)
case "updateAttrViewColOption":
ret = tx.doUpdateAttrViewColOption(op)
case "setAttrViewColCalc":
ret = tx.doSetAttrViewColCalc(op)
case "updateAttrViewColNumberFormat":
ret = tx.doUpdateAttrViewColNumberFormat(op)
case "replaceAttrViewBlock":
ret = tx.doReplaceAttrViewBlock(op)
case "updateAttrViewColTemplate":
ret = tx.doUpdateAttrViewColTemplate(op)
case "addAttrViewView":
ret = tx.doAddAttrViewView(op)
case "removeAttrViewView":
ret = tx.doRemoveAttrViewView(op)
case "setAttrViewViewName":
ret = tx.doSetAttrViewViewName(op)
case "setAttrViewViewIcon":
ret = tx.doSetAttrViewViewIcon(op)
case "duplicateAttrViewView":
ret = tx.doDuplicateAttrViewView(op)
case "sortAttrViewView":
ret = tx.doSortAttrViewView(op)
case "updateAttrViewColRelation":
ret = tx.doUpdateAttrViewColRelation(op)
case "updateAttrViewColRollup":
ret = tx.doUpdateAttrViewColRollup(op)
case "hideAttrViewName":
ret = tx.doHideAttrViewName(op)
case "setAttrViewColDate":
ret = tx.doSetAttrViewColDate(op)
case "unbindAttrViewBlock":
ret = tx.doUnbindAttrViewBlock(op)
case "duplicateAttrViewKey":
ret = tx.doDuplicateAttrViewKey(op)
}
if nil != ret {
tx.rollback()
return
}
}
if cr := tx.commit(); nil != cr {
logging.LogErrorf("commit tx failed: %s", cr)
return &TxErr{msg: cr.Error()}
}
return
}
func (tx *Transaction) doMove(operation *Operation) (ret *TxErr) {
var err error
id := operation.ID
srcTree, err := tx.loadTree(id)
if err != nil {
logging.LogErrorf("load tree [%s] failed: %s", id, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
srcNode := treenode.GetNodeInTree(srcTree, id)
if nil == srcNode {
logging.LogErrorf("get node [%s] in tree [%s] failed", id, srcTree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
var headingChildren []*ast.Node
if isMovingFoldHeading := ast.NodeHeading == srcNode.Type && "1" == srcNode.IALAttr("fold"); isMovingFoldHeading {
headingChildren = treenode.HeadingChildren(srcNode)
// Blocks below other non-folded headings are no longer moved when moving a folded heading https://github.com/siyuan-note/siyuan/issues/8321
headingChildren = treenode.GetHeadingFold(headingChildren)
}
var srcEmptyList *ast.Node
if ast.NodeListItem == srcNode.Type && srcNode.Parent.FirstChild == srcNode && srcNode.Parent.LastChild == srcNode {
// 列表中唯一的列表项被移除后,该列表就为空了
srcEmptyList = srcNode.Parent
}
targetPreviousID := operation.PreviousID
targetParentID := operation.ParentID
if "" != targetPreviousID {
if id == targetPreviousID {
return
}
var targetTree *parse.Tree
targetTree, err = tx.loadTree(targetPreviousID)
if err != nil {
logging.LogErrorf("load tree [%s] failed: %s", targetPreviousID, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: targetPreviousID}
}
isSameTree := srcTree.ID == targetTree.ID
if isSameTree {
targetTree = srcTree
}
targetNode := treenode.GetNodeInTree(targetTree, targetPreviousID)
if nil == targetNode {
logging.LogErrorf("get node [%s] in tree [%s] failed", targetPreviousID, targetTree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: targetPreviousID}
}
if ast.NodeHeading == targetNode.Type && "1" == targetNode.IALAttr("fold") {
targetChildren := treenode.HeadingChildren(targetNode)
targetChildren = treenode.GetHeadingFold(targetChildren)
if l := len(targetChildren); 0 < l {
targetNode = targetChildren[l-1]
}
}
if isMovingFoldHeadingIntoSelf(targetNode, headingChildren) {
return
}
for i := len(headingChildren) - 1; -1 < i; i-- {
c := headingChildren[i]
targetNode.InsertAfter(c)
}
targetNode.InsertAfter(srcNode)
if nil != srcEmptyList {
srcEmptyList.Unlink()
}
refreshUpdated(srcNode)
tx.nodes[srcNode.ID] = srcNode
refreshUpdated(srcTree.Root)
if err = tx.writeTree(srcTree); err != nil {
return
}
if !isSameTree {
if err = tx.writeTree(targetTree); err != nil {
return
}
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, 1*time.Second, refreshRefCount, srcTree.ID, srcTree.ID)
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, 1*time.Second, refreshRefCount, targetTree.ID, srcNode.ID)
}
return
}
if id == targetParentID {
return
}
targetTree, err := tx.loadTree(targetParentID)
if err != nil {
logging.LogErrorf("load tree [%s] failed: %s", targetParentID, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: targetParentID}
}
isSameTree := srcTree.ID == targetTree.ID
if isSameTree {
targetTree = srcTree
}
targetNode := treenode.GetNodeInTree(targetTree, targetParentID)
if nil == targetNode {
logging.LogErrorf("get node [%s] in tree [%s] failed", targetParentID, targetTree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: targetParentID}
}
if isMovingFoldHeadingIntoSelf(targetNode, headingChildren) {
return
}
processed := false
if ast.NodeSuperBlock == targetNode.Type {
// 在布局节点后插入
targetNode = targetNode.FirstChild.Next
for i := len(headingChildren) - 1; -1 < i; i-- {
c := headingChildren[i]
targetNode.InsertAfter(c)
}
targetNode.InsertAfter(srcNode)
if nil != srcEmptyList {
srcEmptyList.Unlink()
}
processed = true
} else if ast.NodeListItem == targetNode.Type {
if 3 == targetNode.ListData.Typ {
// 在任务列表标记节点后插入
targetNode = targetNode.FirstChild
for i := len(headingChildren) - 1; -1 < i; i-- {
c := headingChildren[i]
targetNode.InsertAfter(c)
}
targetNode.InsertAfter(srcNode)
if nil != srcEmptyList {
srcEmptyList.Unlink()
}
processed = true
}
}
if !processed {
for i := len(headingChildren) - 1; -1 < i; i-- {
c := headingChildren[i]
targetNode.PrependChild(c)
}
targetNode.PrependChild(srcNode)
if nil != srcEmptyList {
srcEmptyList.Unlink()
}
}
refreshUpdated(srcNode)
tx.nodes[srcNode.ID] = srcNode
refreshUpdated(srcTree.Root)
if err = tx.writeTree(srcTree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
if !isSameTree {
if err = tx.writeTree(targetTree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, 1*time.Second, refreshRefCount, srcTree.ID, srcTree.ID)
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, 1*time.Second, refreshRefCount, targetTree.ID, srcNode.ID)
}
return
}
func isMovingFoldHeadingIntoSelf(targetNode *ast.Node, headingChildren []*ast.Node) bool {
for _, headingChild := range headingChildren {
if headingChild.ID == targetNode.ID {
// 不能将折叠标题移动到自己下方节点的前或后 https://github.com/siyuan-note/siyuan/issues/7163
return true
}
}
return false
}
func (tx *Transaction) doPrependInsert(operation *Operation) (ret *TxErr) {
var err error
block := treenode.GetBlockTree(operation.ParentID)
if nil == block {
logging.LogWarnf("not found block [%s]", operation.ParentID)
util.ReloadUI() // 比如分屏后编辑器状态不一致,这里强制重新载入界面
return
}
tree, err := tx.loadTree(block.ID)
if err != nil {
msg := fmt.Sprintf("load tree [%s] failed: %s", block.ID, err)
logging.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: block.ID}
}
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
subTree := tx.luteEngine.BlockDOM2Tree(data)
insertedNode := subTree.Root.FirstChild
if nil == insertedNode {
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: block.ID}
}
var remains []*ast.Node
for remain := insertedNode.Next; nil != remain; remain = remain.Next {
if ast.NodeKramdownBlockIAL != remain.Type {
if "" == remain.ID {
remain.ID = ast.NewNodeID()
remain.SetIALAttr("id", remain.ID)
}
remains = append(remains, remain)
}
}
if "" == insertedNode.ID {
insertedNode.ID = ast.NewNodeID()
insertedNode.SetIALAttr("id", insertedNode.ID)
}
node := treenode.GetNodeInTree(tree, operation.ParentID)
if nil == node {
logging.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
}
isContainer := node.IsContainerBlock()
for i := len(remains) - 1; 0 <= i; i-- {
remain := remains[i]
if isContainer {
if ast.NodeListItem == node.Type && 3 == node.ListData.Typ {
node.FirstChild.InsertAfter(remain)
} else if ast.NodeSuperBlock == node.Type {
node.FirstChild.Next.InsertAfter(remain)
} else {
node.PrependChild(remain)
}
} else {
node.InsertAfter(remain)
}
}
if isContainer {
if ast.NodeListItem == node.Type && 3 == node.ListData.Typ {
node.FirstChild.InsertAfter(insertedNode)
} else if ast.NodeSuperBlock == node.Type {
node.FirstChild.Next.InsertAfter(insertedNode)
} else {
node.PrependChild(insertedNode)
}
} else {
node.InsertAfter(insertedNode)
}
createdUpdated(insertedNode)
tx.nodes[insertedNode.ID] = insertedNode
if err = tx.writeTree(tree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: block.ID}
}
operation.ID = insertedNode.ID
operation.ParentID = insertedNode.Parent.ID
// 将 prependInsert 转换为 insert 推送
operation.Action = "insert"
if nil != insertedNode.Previous {
operation.PreviousID = insertedNode.Previous.ID
}
return
}
func (tx *Transaction) doAppendInsert(operation *Operation) (ret *TxErr) {
var err error
block := treenode.GetBlockTree(operation.ParentID)
if nil == block {
logging.LogWarnf("not found block [%s]", operation.ParentID)
util.ReloadUI() // 比如分屏后编辑器状态不一致,这里强制重新载入界面
return
}
tree, err := tx.loadTree(block.ID)
if err != nil {
msg := fmt.Sprintf("load tree [%s] failed: %s", block.ID, err)
logging.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: block.ID}
}
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
subTree := tx.luteEngine.BlockDOM2Tree(data)
insertedNode := subTree.Root.FirstChild
if nil == insertedNode {
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: block.ID}
}
if "" == insertedNode.ID {
insertedNode.ID = ast.NewNodeID()
insertedNode.SetIALAttr("id", insertedNode.ID)
}
var toInserts []*ast.Node
for toInsert := insertedNode; nil != toInsert; toInsert = toInsert.Next {
if ast.NodeKramdownBlockIAL != toInsert.Type {
if "" == toInsert.ID {
toInsert.ID = ast.NewNodeID()
toInsert.SetIALAttr("id", toInsert.ID)
}
toInserts = append(toInserts, toInsert)
}
}
node := treenode.GetNodeInTree(tree, operation.ParentID)
if nil == node {
logging.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
}
isContainer := node.IsContainerBlock()
for i := 0; i < len(toInserts); i++ {
toInsert := toInserts[i]
if isContainer {
if ast.NodeList == node.Type {
// 列表下只能挂列表项,所以这里需要分情况处理 https://github.com/siyuan-note/siyuan/issues/9955
if ast.NodeList == toInsert.Type {
var childLis []*ast.Node
for childLi := toInsert.FirstChild; nil != childLi; childLi = childLi.Next {
childLis = append(childLis, childLi)
}
for _, childLi := range childLis {
node.AppendChild(childLi)
}
} else {
newLiID := ast.NewNodeID()
newLi := &ast.Node{ID: newLiID, Type: ast.NodeListItem, ListData: &ast.ListData{Typ: node.ListData.Typ}}
newLi.SetIALAttr("id", newLiID)
node.AppendChild(newLi)
newLi.AppendChild(toInsert)
}
} else if ast.NodeSuperBlock == node.Type {
node.LastChild.InsertBefore(toInsert)
} else {
node.AppendChild(toInsert)
}
} else {
node.InsertAfter(toInsert)
}
}
createdUpdated(insertedNode)
tx.nodes[insertedNode.ID] = insertedNode
if err = tx.writeTree(tree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: block.ID}
}
operation.ID = insertedNode.ID
operation.ParentID = insertedNode.Parent.ID
// 将 appendInsert 转换为 insert 推送
operation.Action = "insert"
if nil != insertedNode.Previous {
operation.PreviousID = insertedNode.Previous.ID
}
return
}
func (tx *Transaction) doAppend(operation *Operation) (ret *TxErr) {
var err error
id := operation.ID
srcTree, err := tx.loadTree(id)
if err != nil {
logging.LogErrorf("load tree [%s] failed: %s", id, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
srcNode := treenode.GetNodeInTree(srcTree, id)
if nil == srcNode {
logging.LogErrorf("get node [%s] in tree [%s] failed", id, srcTree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
if ast.NodeDocument == srcNode.Type {
logging.LogWarnf("can't append a root to another root")
return
}
var headingChildren []*ast.Node
if isMovingFoldHeading := ast.NodeHeading == srcNode.Type && "1" == srcNode.IALAttr("fold"); isMovingFoldHeading {
headingChildren = treenode.HeadingChildren(srcNode)
}
var srcEmptyList, targetNewList *ast.Node
if ast.NodeListItem == srcNode.Type {
targetNewListID := ast.NewNodeID()
targetNewList = &ast.Node{ID: targetNewListID, Type: ast.NodeList, ListData: &ast.ListData{Typ: srcNode.ListData.Typ}}
targetNewList.SetIALAttr("id", targetNewListID)
if srcNode.Parent.FirstChild == srcNode && srcNode.Parent.LastChild == srcNode {
// 列表中唯一的列表项被移除后,该列表就为空了
srcEmptyList = srcNode.Parent
}
}
targetRootID := operation.ParentID
if id == targetRootID {
logging.LogWarnf("target root id is nil")
return
}
targetTree, err := tx.loadTree(targetRootID)
if err != nil {
logging.LogErrorf("load tree [%s] failed: %s", targetRootID, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: targetRootID}
}
isSameTree := srcTree.ID == targetTree.ID
if isSameTree {
targetTree = srcTree
}
targetRoot := targetTree.Root
if nil != targetNewList {
if nil != targetRoot.LastChild {
if ast.NodeList != targetRoot.LastChild.Type {
targetNewList.AppendChild(srcNode)
targetRoot.AppendChild(targetNewList)
} else {
targetRoot.LastChild.AppendChild(srcNode)
}
} else {
targetRoot.AppendChild(srcNode)
}
} else {
targetRoot.AppendChild(srcNode)
}
for _, c := range headingChildren {
targetRoot.AppendChild(c)
}
if nil != srcEmptyList {
srcEmptyList.Unlink()
}
if err = tx.writeTree(srcTree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
if !isSameTree {
if err = tx.writeTree(targetTree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
}
return
}
func (tx *Transaction) doDelete(operation *Operation) (ret *TxErr) {
// logging.LogInfof("commit delete [%+v]", operation)
var err error
id := operation.ID
tree, err := tx.loadTree(id)
if err != nil {
if errors.Is(err, ErrBlockNotFound) {
// move 以后这里会空,算作正常情况
return
}
msg := fmt.Sprintf("load tree [%s] failed: %s", id, err)
logging.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
return nil // move 以后的情况,列表项移动导致的状态异常 https://github.com/siyuan-note/insider/issues/961
}
// 收集引用的定义块 ID
refDefIDs := getRefDefIDs(node)
// 推送定义节点引用计数
for _, defID := range refDefIDs {
defTree, _ := LoadTreeByBlockID(defID)
if nil != defTree {
defNode := treenode.GetNodeInTree(defTree, defID)
if nil != defNode {
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, 1*time.Second, refreshRefCount, defTree.ID, defNode.ID)
}
}
}
parent := node.Parent
if nil != node.Next && ast.NodeKramdownBlockIAL == node.Next.Type && bytes.Contains(node.Next.Tokens, []byte(node.ID)) {
// 列表块撤销状态异常 https://github.com/siyuan-note/siyuan/issues/3985
node.Next.Unlink()
}
node.Unlink()
if nil != parent && ast.NodeListItem == parent.Type && nil == parent.FirstChild {
// 保持空列表项
node.FirstChild = nil
parent.AppendChild(node)
}
treenode.RemoveBlockTree(node.ID)
delete(tx.nodes, node.ID)
if err = tx.writeTree(tree); err != nil {
return
}
// 如果是断开列表时的删除列表项事务,则不需要删除数据库绑定块,因为断开列表事务后面会再次插入相同 ID 的列表项
// List item disconnection no longer affects database binding blocks https://github.com/siyuan-note/siyuan/issues/12235
needSyncDel2AvBlock := true
if ast.NodeListItem == node.Type {
for _, op := range tx.DoOperations {
// 不可能出现相同 ID 先插入再删除的情况,只可能出现先删除再插入的情况,所以这里只需要查找插入操作
if "insert" == op.Action {
data := strings.ReplaceAll(op.Data.(string), editor.FrontEndCaret, "")
subTree := tx.luteEngine.BlockDOM2Tree(data)
ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || ast.NodeListItem != n.Type {
return ast.WalkContinue
}
if n.ID == operation.ID {
needSyncDel2AvBlock = false
return ast.WalkStop
}
return ast.WalkContinue
})
break
}
}
}
if needSyncDel2AvBlock {
syncDelete2AvBlock(node, tree, tx)
}
return
}
func syncDelete2AvBlock(node *ast.Node, nodeTree *parse.Tree, tx *Transaction) {
changedAvIDs := syncDelete2AttributeView(node)
avIDs := tx.syncDelete2Block(node, nodeTree)
changedAvIDs = append(changedAvIDs, avIDs...)
changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
for _, avID := range changedAvIDs {
ReloadAttrView(avID)
}
}
func (tx *Transaction) syncDelete2Block(node *ast.Node, nodeTree *parse.Tree) (changedAvIDs []string) {
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || ast.NodeAttributeView != n.Type {
return ast.WalkContinue
}
avID := n.AttributeViewID
isMirror := av.IsMirror(avID)
if changed := av.RemoveBlockRel(avID, n.ID, treenode.ExistBlockTree); changed {
changedAvIDs = append(changedAvIDs, avID)
}
if isMirror {
// 删除镜像数据库节点后不需要解绑块,因为其他镜像节点还在使用
return ast.WalkContinue
}
attrView, err := av.ParseAttributeView(avID)
if err != nil {
return ast.WalkContinue
}
trees, nodes := tx.getAttrViewBoundNodes(attrView)
for _, toChangNode := range nodes {
avs := toChangNode.IALAttr(av.NodeAttrNameAvs)
if "" != avs {
avIDs := strings.Split(avs, ",")
avIDs = gulu.Str.RemoveElem(avIDs, avID)
if 1 > len(avIDs) {
toChangNode.RemoveIALAttr(av.NodeAttrNameAvs)
} else {
toChangNode.SetIALAttr(av.NodeAttrNameAvs, strings.Join(avIDs, ","))
}
}
avNames := getAvNames(toChangNode.IALAttr(av.NodeAttrNameAvs))
oldAttrs := parse.IAL2Map(toChangNode.KramdownIAL)
toChangNode.SetIALAttr(av.NodeAttrViewNames, avNames)
pushBroadcastAttrTransactions(oldAttrs, toChangNode)
}
nodeTreeID := nodeTree.ID
for _, tree := range trees {
self := nodeTreeID == tree.ID
if !self {
indexWriteTreeUpsertQueue(tree)
}
}
return ast.WalkContinue
})
changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
return
}
func syncDelete2AttributeView(node *ast.Node) (changedAvIDs []string) {
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || !n.IsBlock() {
return ast.WalkContinue
}
avs := n.IALAttr(av.NodeAttrNameAvs)
if "" == avs {
return ast.WalkContinue
}
avIDs := strings.Split(avs, ",")
for _, avID := range avIDs {
attrView, parseErr := av.ParseAttributeView(avID)
if nil != parseErr {
continue
}
changedAv := false
blockValues := attrView.GetBlockKeyValues()
if nil == blockValues {
continue
}
for i, blockValue := range blockValues.Values {
if blockValue.Block.ID == n.ID {
blockValues.Values = append(blockValues.Values[:i], blockValues.Values[i+1:]...)
changedAv = true
break
}
}
if changedAv {
av.SaveAttributeView(attrView)
changedAvIDs = append(changedAvIDs, avID)
}
}
return ast.WalkContinue
})
changedAvIDs = gulu.Str.RemoveDuplicatedElem(changedAvIDs)
return
}
func (tx *Transaction) doInsert(operation *Operation) (ret *TxErr) {
var err error
opParentID := operation.ParentID
block := treenode.GetBlockTree(opParentID)
if nil == block {
block = treenode.GetBlockTree(operation.PreviousID)
if nil == block {
block = treenode.GetBlockTree(operation.NextID)
}
}
if nil == block {
logging.LogWarnf("not found block [%s, %s, %s]", operation.ParentID, operation.PreviousID, operation.NextID)
util.ReloadUI() // 比如分屏后编辑器状态不一致,这里强制重新载入界面
return
}
tree, err := tx.loadTree(block.ID)
if err != nil {
msg := fmt.Sprintf("load tree [%s] failed: %s", block.ID, err)
logging.LogErrorf(msg)
return &TxErr{code: TxErrCodeBlockNotFound, id: block.ID}
}
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
subTree := tx.luteEngine.BlockDOM2Tree(data)
p := block.Path
assets := getAssetsDir(filepath.Join(util.DataDir, block.BoxID), filepath.Dir(filepath.Join(util.DataDir, block.BoxID, p)))
isGlobalAssets := strings.HasPrefix(assets, filepath.Join(util.DataDir, "assets"))
if !isGlobalAssets {
// 本地资源文件需要移动到用户手动建立的 assets 下 https://github.com/siyuan-note/siyuan/issues/2410
ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeLinkDest == n.Type && bytes.HasPrefix(n.Tokens, []byte("assets/")) {
assetP := gulu.Str.FromBytes(n.Tokens)
assetPath, e := GetAssetAbsPath(assetP)
if nil != e {
logging.LogErrorf("get path of asset [%s] failed: %s", assetP, err)
return ast.WalkContinue
}
if !strings.HasPrefix(assetPath, filepath.Join(util.DataDir, "assets")) {
// 非全局 assets 则跳过
return ast.WalkContinue
}
// 只有全局 assets 才移动到相对 assets
targetP := filepath.Join(assets, filepath.Base(assetPath))
if e = filelock.Rename(assetPath, targetP); err != nil {
logging.LogErrorf("copy path of asset from [%s] to [%s] failed: %s", assetPath, targetP, err)
return ast.WalkContinue
}
}
return ast.WalkContinue
})
}
insertedNode := subTree.Root.FirstChild
if nil == insertedNode {
return &TxErr{code: TxErrCodeBlockNotFound, msg: "invalid data tree", id: block.ID}
}
var remains []*ast.Node
for remain := insertedNode.Next; nil != remain; remain = remain.Next {
if ast.NodeKramdownBlockIAL != remain.Type {
if "" == remain.ID {
remain.ID = ast.NewNodeID()
remain.SetIALAttr("id", remain.ID)
}
remains = append(remains, remain)
}
}
if "" == insertedNode.ID {
insertedNode.ID = ast.NewNodeID()
insertedNode.SetIALAttr("id", insertedNode.ID)
}
var node *ast.Node
nextID := operation.NextID
previousID := operation.PreviousID
if "" != nextID {
node = treenode.GetNodeInTree(tree, nextID)
if nil == node {
logging.LogErrorf("get node [%s] in tree [%s] failed", nextID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: nextID}
}
if ast.NodeList == insertedNode.Type && nil != node.Parent && ast.NodeList == node.Parent.Type {
insertedNode = insertedNode.FirstChild
}
node.InsertBefore(insertedNode)
} else if "" != previousID {
node = treenode.GetNodeInTree(tree, previousID)
if nil == node {
logging.LogErrorf("get node [%s] in tree [%s] failed", previousID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: previousID}
}
if ast.NodeHeading == node.Type && "1" == node.IALAttr("fold") {
children := treenode.HeadingChildren(node)
if l := len(children); 0 < l {
node = children[l-1]
}
}
if ast.NodeList == insertedNode.Type && nil != node.Parent && ast.NodeList == node.Parent.Type {
insertedNode = insertedNode.FirstChild
}
for i := len(remains) - 1; 0 <= i; i-- {
remain := remains[i]
node.InsertAfter(remain)
}
node.InsertAfter(insertedNode)
} else {
node = treenode.GetNodeInTree(tree, operation.ParentID)
if nil == node {
logging.LogErrorf("get node [%s] in tree [%s] failed", operation.ParentID, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: operation.ParentID}
}
if ast.NodeSuperBlock == node.Type {
// 在布局节点后插入
node.FirstChild.Next.InsertAfter(insertedNode)
} else {
if ast.NodeList == insertedNode.Type && nil != insertedNode.FirstChild && operation.ID == insertedNode.FirstChild.ID && operation.ID != insertedNode.ID {
// 将一个列表项移动到另一个列表的第一项时 https://github.com/siyuan-note/siyuan/issues/2341
insertedNode = insertedNode.FirstChild
}
if ast.NodeListItem == node.Type && 3 == node.ListData.Typ {
// 在任务列表标记节点后插入
node.FirstChild.InsertAfter(insertedNode)
for _, remain := range remains {
node.FirstChild.InsertAfter(remain)
}
} else {
for i := len(remains) - 1; 0 <= i; i-- {
remain := remains[i]
node.PrependChild(remain)
}
node.PrependChild(insertedNode)
}
}
}
createdUpdated(insertedNode)
tx.nodes[insertedNode.ID] = insertedNode
if err = tx.writeTree(tree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: block.ID}
}
// 收集引用的定义块 ID
refDefIDs := getRefDefIDs(insertedNode)
// 推送定义节点引用计数
for _, defID := range refDefIDs {
defTree, _ := LoadTreeByBlockID(defID)
if nil != defTree {
defNode := treenode.GetNodeInTree(defTree, defID)
if nil != defNode {
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, 1*time.Second, refreshRefCount, defTree.ID, defNode.ID)
}
}
}
upsertAvBlockRel(insertedNode)
// 复制为副本时将该副本块插入到数据库中 https://github.com/siyuan-note/siyuan/issues/11959
avs := insertedNode.IALAttr(av.NodeAttrNameAvs)
for _, avID := range strings.Split(avs, ",") {
if !ast.IsNodeIDPattern(avID) {
continue
}
AddAttributeViewBlock(tx, []map[string]interface{}{{
"id": insertedNode.ID,
"isDetached": false,
}}, avID, "", previousID, false)
ReloadAttrView(avID)
}
// 插入数据库块时需要重新绑定其中已经存在的块
// 比如剪切操作时,会先进行 delete 数据库解绑块,这里需要重新绑定 https://github.com/siyuan-note/siyuan/issues/13031
if ast.NodeAttributeView == insertedNode.Type {
attrView, parseErr := av.ParseAttributeView(insertedNode.AttributeViewID)
if nil == parseErr {
trees, toBindNodes := tx.getAttrViewBoundNodes(attrView)
for _, toBindNode := range toBindNodes {
t := trees[toBindNode.ID]
bindBlockAv0(tx, insertedNode.AttributeViewID, toBindNode, t)
}
}
}
operation.ID = insertedNode.ID
operation.ParentID = insertedNode.Parent.ID
checkUpsertInUserGuide(tree)
return
}
func (tx *Transaction) doUpdate(operation *Operation) (ret *TxErr) {
id := operation.ID
tree, err := tx.loadTree(id)
if err != nil {
logging.LogErrorf("load tree [%s] failed: %s", id, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
data := strings.ReplaceAll(operation.Data.(string), editor.FrontEndCaret, "")
if "" == data {
logging.LogErrorf("update data is nil")
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
subTree := tx.luteEngine.BlockDOM2Tree(data)
subTree.ID, subTree.Box, subTree.Path = tree.ID, tree.Box, tree.Path
oldNode := treenode.GetNodeInTree(tree, id)
if nil == oldNode {
logging.LogErrorf("get node [%s] in tree [%s] failed", id, tree.Root.ID)
return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
}
// 收集引用的定义块 ID
oldDefIDs := getRefDefIDs(oldNode)
var newDefIDs []string
var unlinks []*ast.Node
ast.Walk(subTree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeTextMark == n.Type {
if n.IsTextMarkType("inline-math") {
if "" == strings.TrimSpace(n.TextMarkInlineMathContent) {
// 剔除空白的行级公式
unlinks = append(unlinks, n)
}
} else if n.IsTextMarkType("block-ref") {
sql.CacheRef(subTree, n)
if "d" == n.TextMarkBlockRefSubtype {
// 偶发编辑文档标题后引用处的动态锚文本不更新 https://github.com/siyuan-note/siyuan/issues/5891
// 使用缓存的动态锚文本强制覆盖当前块中的引用节点动态锚文本
if dRefText, ok := treenode.DynamicRefTexts.Load(n.TextMarkBlockRefID); ok && "" != dRefText {
n.TextMarkTextContent = dRefText.(string)
}
}
newDefIDs = append(newDefIDs, n.TextMarkBlockRefID)
}
}
return ast.WalkContinue
})
for _, n := range unlinks {
n.Unlink()
}
oldDefIDs = gulu.Str.RemoveDuplicatedElem(oldDefIDs)
newDefIDs = gulu.Str.RemoveDuplicatedElem(newDefIDs)
refDefIDs := oldDefIDs
if !slices.Equal(oldDefIDs, newDefIDs) { // 如果引用发生了变化,则推送定义节点引用计数
refDefIDs = append(refDefIDs, newDefIDs...)
refDefIDs = gulu.Str.RemoveDuplicatedElem(refDefIDs)
for _, defID := range refDefIDs {
defTree, _ := LoadTreeByBlockID(defID)
if nil != defTree {
defNode := treenode.GetNodeInTree(defTree, defID)
if nil != defNode {
task.AppendAsyncTaskWithDelay(task.SetDefRefCount, 1*time.Second, refreshRefCount, defTree.ID, defNode.ID)
}
}
}
}
updatedNode := subTree.Root.FirstChild
if nil == updatedNode {
logging.LogErrorf("get fist node in sub tree [%s] failed", subTree.Root.ID)
return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
}
if ast.NodeList == updatedNode.Type && ast.NodeList == oldNode.Parent.Type {
updatedNode = updatedNode.FirstChild
}
if oldNode.IsContainerBlock() {
// 更新容器块的话需要考虑其子块中可能存在的折叠标题,需要把这些折叠标题的下方块移动到新节点下面
treenode.MoveFoldHeading(updatedNode, oldNode)
}
cache.PutBlockIAL(updatedNode.ID, parse.IAL2Map(updatedNode.KramdownIAL))
// 替换为新节点
oldNode.InsertAfter(updatedNode)
oldNode.Unlink()
createdUpdated(updatedNode)
tx.nodes[updatedNode.ID] = updatedNode
if err = tx.writeTree(tree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
upsertAvBlockRel(updatedNode)
checkUpsertInUserGuide(tree)
return
}
func getRefDefIDs(node *ast.Node) (refDefIDs []string) {
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if treenode.IsBlockRef(n) {
refDefIDs = append(refDefIDs, n.TextMarkBlockRefID)
} else if treenode.IsEmbedBlockRef(n) {
defID := treenode.GetEmbedBlockRef(n)
refDefIDs = append(refDefIDs, defID)
}
return ast.WalkContinue
})
refDefIDs = gulu.Str.RemoveDuplicatedElem(refDefIDs)
return
}
func upsertAvBlockRel(node *ast.Node) {
var affectedAvIDs []string
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if ast.NodeAttributeView == n.Type {
avID := n.AttributeViewID
if changed := av.UpsertBlockRel(avID, n.ID); changed {
affectedAvIDs = append(affectedAvIDs, avID)
}
}
return ast.WalkContinue
})
updatedNodes := []*ast.Node{node}
var parents []*ast.Node
for parent := node.Parent; nil != parent && ast.NodeDocument != parent.Type; parent = parent.Parent {
parents = append(parents, parent)
}
updatedNodes = append(updatedNodes, parents...)
for _, updatedNode := range updatedNodes {
ast.Walk(updatedNode, func(n *ast.Node, entering bool) ast.WalkStatus {
avs := n.IALAttr(av.NodeAttrNameAvs)
if "" == avs {
return ast.WalkContinue
}
avIDs := strings.Split(avs, ",")
affectedAvIDs = append(affectedAvIDs, avIDs...)
return ast.WalkContinue
})
}
affectedAvIDs = gulu.Str.RemoveDuplicatedElem(affectedAvIDs)
go func() {
time.Sleep(100 * time.Millisecond)
sql.FlushQueue()
for _, avID := range affectedAvIDs {
ReloadAttrView(avID)
}
}()
}
func (tx *Transaction) doUpdateUpdated(operation *Operation) (ret *TxErr) {
id := operation.ID
tree, err := tx.loadTree(id)
if err != nil {
if errors.Is(err, ErrBlockNotFound) {
logging.LogWarnf("not found block [%s]", id)
return
}
logging.LogErrorf("load tree [%s] failed: %s", id, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
logging.LogErrorf("get node [%s] in tree [%s] failed", id, tree.Root.ID)
return &TxErr{msg: ErrBlockNotFound.Error(), id: id}
}
node.SetIALAttr("updated", operation.Data.(string))
createdUpdated(node)
tx.nodes[node.ID] = node
if err = tx.writeTree(tree); err != nil {
return &TxErr{code: TxErrCodeWriteTree, msg: err.Error(), id: id}
}
return
}
func (tx *Transaction) doCreate(operation *Operation) (ret *TxErr) {
tree := operation.Data.(*parse.Tree)
tx.writeTree(tree)
checkUpsertInUserGuide(tree)
return
}
func (tx *Transaction) doSetAttrs(operation *Operation) (ret *TxErr) {
id := operation.ID
tree, err := tx.loadTree(id)
if err != nil {
logging.LogErrorf("load tree [%s] failed: %s", id, err)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
node := treenode.GetNodeInTree(tree, id)
if nil == node {
logging.LogErrorf("get node [%s] in tree [%s] failed", id, tree.Root.ID)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
attrs := map[string]string{}
if err = gulu.JSON.UnmarshalJSON([]byte(operation.Data.(string)), &attrs); err != nil {
logging.LogErrorf("unmarshal attrs failed: %s", err)
return &TxErr{code: TxErrCodeBlockNotFound, id: id}
}
var invalidNames []string
for name := range attrs {
for i := 0; i < len(name); i++ {
if !lex.IsASCIILetterNumHyphen(name[i]) {
logging.LogWarnf("invalid attr name [%s]", name)
invalidNames = append(invalidNames, name)
}
}
}
for _, name := range invalidNames {
delete(attrs, name)
}
for name, value := range attrs {
if "" == value {
node.RemoveIALAttr(name)
} else {
node.SetIALAttr(name, value)
}
}
if err = tx.writeTree(tree); err != nil {
return
}
cache.PutBlockIAL(id, parse.IAL2Map(node.KramdownIAL))
return
}
func refreshUpdated(node *ast.Node) {
updated := util.CurrentTimeSecondsStr()
node.SetIALAttr("updated", updated)
parents := treenode.ParentNodesWithHeadings(node)
for _, parent := range parents { // 更新所有父节点的更新时间字段
parent.SetIALAttr("updated", updated)
}
}
func createdUpdated(node *ast.Node) {
// 补全子节点的更新时间 Improve block update time filling https://github.com/siyuan-note/siyuan/issues/12182
ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering || !n.IsBlock() || ast.NodeKramdownBlockIAL == n.Type {
return ast.WalkContinue
}
updated := n.IALAttr("updated")
if "" == updated && ast.IsNodeIDPattern(n.ID) {
created := util.TimeFromID(n.ID)
n.SetIALAttr("updated", created)
}
return ast.WalkContinue
})
created := util.TimeFromID(node.ID)
updated := node.IALAttr("updated")
if "" == updated {
updated = created
}
if updated < created {
updated = created
}
parents := treenode.ParentNodesWithHeadings(node)
for _, parent := range parents { // 更新所有父节点的更新时间字段
parent.SetIALAttr("updated", updated)
cache.PutBlockIAL(parent.ID, parse.IAL2Map(parent.KramdownIAL))
}
}
type Operation struct {
Action string `json:"action"`
Data interface{} `json:"data"`
ID string `json:"id"`
ParentID string `json:"parentID"`
PreviousID string `json:"previousID"`
NextID string `json:"nextID"`
RetData interface{} `json:"retData"`
BlockIDs []string `json:"blockIDs"`
BlockID string `json:"blockID"`
DeckID string `json:"deckID"` // 用于添加/删除闪卡
AvID string `json:"avID"` // 属性视图 ID
SrcIDs []string `json:"srcIDs"` // 用于从属性视图中删除行
Srcs []map[string]interface{} `json:"srcs"` // 用于添加属性视图行(包括绑定块){id, content, isDetached}
IsDetached bool `json:"isDetached"` // 用于标识是否未绑定块,仅存在于属性视图中
IgnoreFillFilterVal bool `json:"ignoreFillFilter"` // 用于标识是否忽略填充筛选值
Name string `json:"name"` // 属性视图列名
Typ string `json:"type"` // 属性视图列类型
Format string `json:"format"` // 属性视图列格式化
KeyID string `json:"keyID"` // 属性视列 ID
RowID string `json:"rowID"` // 属性视图行 ID
IsTwoWay bool `json:"isTwoWay"` // 属性视图关联列是否是双向关系
BackRelationKeyID string `json:"backRelationKeyID"` // 属性视图关联列回链关联列的 ID
RemoveDest bool `json:"removeDest"` // 属性视图删除关联目标
}
type Transaction struct {
Timestamp int64 `json:"timestamp"`
DoOperations []*Operation `json:"doOperations"`
UndoOperations []*Operation `json:"undoOperations"`
trees map[string]*parse.Tree
nodes map[string]*ast.Node
luteEngine *lute.Lute
m *sync.Mutex
state atomic.Int32 // 0: 初始化1未提交:2: 已提交3: 已回滚
}
func (tx *Transaction) WaitForCommit() {
for {
if 1 == tx.state.Load() {
time.Sleep(10 * time.Millisecond)
continue
}
return
}
}
func (tx *Transaction) begin() (err error) {
tx.trees = map[string]*parse.Tree{}
tx.nodes = map[string]*ast.Node{}
tx.luteEngine = util.NewLute()
tx.m.Lock()
tx.state.Store(1)
return
}
func (tx *Transaction) commit() (err error) {
for _, tree := range tx.trees {
if err = writeTreeUpsertQueue(tree); err != nil {
return
}
var sources []interface{}
sources = append(sources, tx)
util.PushSaveDoc(tree.ID, "tx", sources)
}
refreshDynamicRefTexts(tx.nodes, tx.trees)
IncSync()
tx.state.Store(2)
tx.m.Unlock()
return
}
func (tx *Transaction) rollback() {
tx.trees, tx.nodes = nil, nil
tx.state.Store(3)
tx.m.Unlock()
return
}
func (tx *Transaction) loadTree(id string) (ret *parse.Tree, err error) {
var rootID, box, p string
bt := treenode.GetBlockTree(id)
if nil == bt {
return nil, ErrBlockNotFound
}
rootID = bt.RootID
box = bt.BoxID
p = bt.Path
ret = tx.trees[rootID]
if nil != ret {
return
}
ret, err = filesys.LoadTree(box, p, tx.luteEngine)
if err != nil {
return
}
tx.trees[rootID] = ret
return
}
func (tx *Transaction) writeTree(tree *parse.Tree) (err error) {
tx.trees[tree.ID] = tree
treenode.UpsertBlockTree(tree)
return
}
func getRefsCacheByDefNode(updateNode *ast.Node) (ret []*sql.Ref, changedParentNodes []*ast.Node) {
ret = sql.GetRefsCacheByDefID(updateNode.ID)
if nil != updateNode.Parent && ast.NodeDocument != updateNode.Parent.Type &&
updateNode.Parent.IsContainerBlock() && updateNode == treenode.FirstLeafBlock(updateNode.Parent) { // 容器块下第一个叶子块
// 如果是容器块下第一个叶子块,则需要向上查找引用
for parent := updateNode.Parent; nil != parent; parent = parent.Parent {
if ast.NodeDocument == parent.Type {
break
}
parentRefs := sql.GetRefsCacheByDefID(parent.ID)
if 0 < len(parentRefs) {
ret = append(ret, parentRefs...)
changedParentNodes = append(changedParentNodes, parent)
}
}
}
return
}
var updateRefTextRenameDocs = map[string]*parse.Tree{}
var updateRefTextRenameDocLock = sync.Mutex{}
func updateRefTextRenameDoc(renamedTree *parse.Tree) {
updateRefTextRenameDocLock.Lock()
updateRefTextRenameDocs[renamedTree.ID] = renamedTree
updateRefTextRenameDocLock.Unlock()
}
func FlushUpdateRefTextRenameDocJob() {
sql.FlushQueue()
flushUpdateRefTextRenameDoc()
}
func flushUpdateRefTextRenameDoc() {
updateRefTextRenameDocLock.Lock()
defer updateRefTextRenameDocLock.Unlock()
for _, tree := range updateRefTextRenameDocs {
refreshDynamicRefText(tree.Root, tree)
}
updateRefTextRenameDocs = map[string]*parse.Tree{}
}
type changedDefNode struct {
id string
refText string
refType string // ref-d/ref-s/embed
}
func updateRefText(refNode *ast.Node, changedDefNodes map[string]*ast.Node) (changed bool, defNodes []*changedDefNode) {
ast.Walk(refNode, func(n *ast.Node, entering bool) ast.WalkStatus {
if !entering {
return ast.WalkContinue
}
if treenode.IsBlockRef(n) {
defID, refText, subtype := treenode.GetBlockRef(n)
if "" == defID {
return ast.WalkContinue
}
defNode := changedDefNodes[defID]
if nil == defNode {
return ast.WalkSkipChildren
}
changed = true
if "d" == subtype {
refText = getNodeRefText(defNode)
treenode.SetDynamicBlockRefText(n, refText)
}
defNodes = append(defNodes, &changedDefNode{id: defID, refText: refText, refType: "ref-" + subtype})
return ast.WalkContinue
} else if treenode.IsEmbedBlockRef(n) {
defID := treenode.GetEmbedBlockRef(n)
changed = true
defNodes = append(defNodes, &changedDefNode{id: defID, refType: "embed"})
return ast.WalkContinue
}
return ast.WalkContinue
})
return
}
func checkUpsertInUserGuide(tree *parse.Tree) {
// In production mode, data reset warning pops up when editing data in the user guide https://github.com/siyuan-note/siyuan/issues/9757
if "prod" == util.Mode && IsUserGuide(tree.Box) {
util.PushErrMsg(Conf.Language(52), 7000)
}
}