vendor tsgo
This commit is contained in:
62
tools/tsgo/internal/project/dirty/box.go
Normal file
62
tools/tsgo/internal/project/dirty/box.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package dirty
|
||||
|
||||
type Box[T Cloneable[T]] struct {
|
||||
original T
|
||||
value T
|
||||
dirty bool
|
||||
delete bool
|
||||
}
|
||||
|
||||
func NewBox[T Cloneable[T]](original T) *Box[T] {
|
||||
return &Box[T]{original: original, value: original}
|
||||
}
|
||||
|
||||
func (b *Box[T]) Value() T {
|
||||
if b.delete {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
return b.value
|
||||
}
|
||||
|
||||
func (b *Box[T]) Original() T {
|
||||
return b.original
|
||||
}
|
||||
|
||||
func (b *Box[T]) Dirty() bool {
|
||||
return b.dirty
|
||||
}
|
||||
|
||||
func (b *Box[T]) Set(value T) {
|
||||
b.value = value
|
||||
b.delete = false
|
||||
b.dirty = true
|
||||
}
|
||||
|
||||
func (b *Box[T]) Change(apply func(T)) {
|
||||
if !b.dirty {
|
||||
b.value = b.value.Clone()
|
||||
b.dirty = true
|
||||
}
|
||||
apply(b.value)
|
||||
}
|
||||
|
||||
func (b *Box[T]) ChangeIf(cond func(T) bool, apply func(T)) bool {
|
||||
if cond(b.value) {
|
||||
b.Change(apply)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *Box[T]) Delete() {
|
||||
b.delete = true
|
||||
}
|
||||
|
||||
func (b *Box[T]) Locked(fn func(Value[T])) {
|
||||
fn(b)
|
||||
}
|
||||
|
||||
func (b *Box[T]) Finalize() (T, bool) {
|
||||
return b.Value(), b.dirty || b.delete
|
||||
}
|
||||
9
tools/tsgo/internal/project/dirty/cloneablemap.go
Normal file
9
tools/tsgo/internal/project/dirty/cloneablemap.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package dirty
|
||||
|
||||
import "maps"
|
||||
|
||||
type CloneableMap[K comparable, V any] map[K]V
|
||||
|
||||
func (m CloneableMap[K, V]) Clone() CloneableMap[K, V] {
|
||||
return maps.Clone(m)
|
||||
}
|
||||
29
tools/tsgo/internal/project/dirty/entry.go
Normal file
29
tools/tsgo/internal/project/dirty/entry.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package dirty
|
||||
|
||||
type mapEntry[K comparable, V any] struct {
|
||||
key K
|
||||
original V
|
||||
value V
|
||||
dirty bool
|
||||
delete bool
|
||||
}
|
||||
|
||||
func (e *mapEntry[K, V]) Key() K {
|
||||
return e.key
|
||||
}
|
||||
|
||||
func (e *mapEntry[K, V]) Original() V {
|
||||
return e.original
|
||||
}
|
||||
|
||||
func (e *mapEntry[K, V]) Value() V {
|
||||
if e.delete {
|
||||
var zero V
|
||||
return zero
|
||||
}
|
||||
return e.value
|
||||
}
|
||||
|
||||
func (e *mapEntry[K, V]) Dirty() bool {
|
||||
return e.dirty
|
||||
}
|
||||
15
tools/tsgo/internal/project/dirty/interfaces.go
Normal file
15
tools/tsgo/internal/project/dirty/interfaces.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package dirty
|
||||
|
||||
type Cloneable[T any] interface {
|
||||
Clone() T
|
||||
}
|
||||
|
||||
type Value[T any] interface {
|
||||
Value() T
|
||||
Original() T
|
||||
Dirty() bool
|
||||
Change(apply func(T))
|
||||
ChangeIf(cond func(T) bool, apply func(T)) bool
|
||||
Delete()
|
||||
Locked(fn func(Value[T]))
|
||||
}
|
||||
169
tools/tsgo/internal/project/dirty/map.go
Normal file
169
tools/tsgo/internal/project/dirty/map.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package dirty
|
||||
|
||||
import "maps"
|
||||
|
||||
type MapEntry[K comparable, V Cloneable[V]] struct {
|
||||
m *Map[K, V]
|
||||
mapEntry[K, V]
|
||||
}
|
||||
|
||||
func (e *MapEntry[K, V]) Change(apply func(V)) {
|
||||
if e.delete {
|
||||
panic("tried to change a deleted entry")
|
||||
}
|
||||
if !e.dirty {
|
||||
e.value = e.value.Clone()
|
||||
e.dirty = true
|
||||
e.m.dirty[e.key] = e
|
||||
}
|
||||
apply(e.value)
|
||||
}
|
||||
|
||||
func (e *MapEntry[K, V]) Replace(newValue V) {
|
||||
if e.delete {
|
||||
panic("tried to change a deleted entry")
|
||||
}
|
||||
if !e.dirty {
|
||||
e.dirty = true
|
||||
e.m.dirty[e.key] = e
|
||||
}
|
||||
e.value = newValue
|
||||
}
|
||||
|
||||
func (e *MapEntry[K, V]) ChangeIf(cond func(V) bool, apply func(V)) bool {
|
||||
if cond(e.Value()) {
|
||||
e.Change(apply)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *MapEntry[K, V]) Delete() {
|
||||
if !e.dirty {
|
||||
e.m.dirty[e.key] = e
|
||||
}
|
||||
e.delete = true
|
||||
}
|
||||
|
||||
func (e *MapEntry[K, V]) Locked(fn func(Value[V])) {
|
||||
fn(e)
|
||||
}
|
||||
|
||||
type Map[K comparable, V Cloneable[V]] struct {
|
||||
base map[K]V
|
||||
dirty map[K]*MapEntry[K, V]
|
||||
}
|
||||
|
||||
func NewMap[K comparable, V Cloneable[V]](base map[K]V) *Map[K, V] {
|
||||
return &Map[K, V]{
|
||||
base: base,
|
||||
dirty: make(map[K]*MapEntry[K, V]),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) Get(key K) (*MapEntry[K, V], bool) {
|
||||
if entry, ok := m.dirty[key]; ok {
|
||||
if entry.delete {
|
||||
return nil, false
|
||||
}
|
||||
return entry, true
|
||||
}
|
||||
value, ok := m.base[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &MapEntry[K, V]{
|
||||
m: m,
|
||||
mapEntry: mapEntry[K, V]{
|
||||
key: key,
|
||||
original: value,
|
||||
value: value,
|
||||
dirty: false,
|
||||
},
|
||||
}, true
|
||||
}
|
||||
|
||||
// Add sets a new entry in the dirty map without checking if it exists
|
||||
// in the base map. The entry added is considered dirty, so it should
|
||||
// be a fresh value, mutable until finalized (i.e., it will not be cloned
|
||||
// before changing if a change is made). If modifying an entry that may
|
||||
// exist in the base map, use `Change` instead.
|
||||
func (m *Map[K, V]) Add(key K, value V) {
|
||||
m.dirty[key] = &MapEntry[K, V]{
|
||||
m: m,
|
||||
mapEntry: mapEntry[K, V]{
|
||||
key: key,
|
||||
value: value,
|
||||
dirty: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) Change(key K, apply func(V)) {
|
||||
if entry, ok := m.Get(key); ok {
|
||||
entry.Change(apply)
|
||||
} else {
|
||||
panic("tried to change a non-existent entry")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) TryDelete(key K) bool {
|
||||
if entry, ok := m.Get(key); ok {
|
||||
entry.Delete()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) Delete(key K) {
|
||||
if !m.TryDelete(key) {
|
||||
panic("tried to delete a non-existent entry")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) Range(fn func(*MapEntry[K, V]) bool) {
|
||||
seenInDirty := make(map[K]struct{})
|
||||
for _, entry := range m.dirty {
|
||||
seenInDirty[entry.key] = struct{}{}
|
||||
if !entry.delete && !fn(entry) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for key, value := range m.base {
|
||||
if _, ok := seenInDirty[key]; ok {
|
||||
continue // already processed in dirty entries
|
||||
}
|
||||
if !fn(&MapEntry[K, V]{m: m, mapEntry: mapEntry[K, V]{
|
||||
key: key,
|
||||
original: value,
|
||||
value: value,
|
||||
dirty: false,
|
||||
}}) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) Clear() {
|
||||
m.dirty = make(map[K]*MapEntry[K, V])
|
||||
m.base = make(map[K]V)
|
||||
}
|
||||
|
||||
func (m *Map[K, V]) Finalize() (result map[K]V, changed bool) {
|
||||
if len(m.dirty) == 0 {
|
||||
return m.base, false // no changes, return base map
|
||||
}
|
||||
if m.base == nil {
|
||||
result = make(map[K]V, len(m.dirty))
|
||||
} else {
|
||||
result = maps.Clone(m.base)
|
||||
}
|
||||
for key, entry := range m.dirty {
|
||||
if entry.delete {
|
||||
delete(result, key)
|
||||
} else {
|
||||
result[key] = entry.value
|
||||
}
|
||||
}
|
||||
return result, true
|
||||
}
|
||||
74
tools/tsgo/internal/project/dirty/mapbuilder.go
Normal file
74
tools/tsgo/internal/project/dirty/mapbuilder.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package dirty
|
||||
|
||||
import "maps"
|
||||
|
||||
type MapBuilder[K comparable, VBase any, VBuilder any] struct {
|
||||
base map[K]VBase
|
||||
dirty map[K]VBuilder
|
||||
deleted map[K]struct{}
|
||||
|
||||
toBuilder func(VBase) VBuilder
|
||||
build func(VBuilder) VBase
|
||||
}
|
||||
|
||||
func NewMapBuilder[K comparable, VBase any, VBuilder any](
|
||||
base map[K]VBase,
|
||||
toBuilder func(VBase) VBuilder,
|
||||
build func(VBuilder) VBase,
|
||||
) *MapBuilder[K, VBase, VBuilder] {
|
||||
return &MapBuilder[K, VBase, VBuilder]{
|
||||
base: base,
|
||||
dirty: make(map[K]VBuilder),
|
||||
toBuilder: toBuilder,
|
||||
build: build,
|
||||
}
|
||||
}
|
||||
|
||||
func (mb *MapBuilder[K, VBase, VBuilder]) Set(key K, value VBuilder) {
|
||||
mb.dirty[key] = value
|
||||
delete(mb.deleted, key)
|
||||
}
|
||||
|
||||
func (mb *MapBuilder[K, VBase, VBuilder]) Delete(key K) {
|
||||
if mb.deleted == nil {
|
||||
mb.deleted = make(map[K]struct{})
|
||||
}
|
||||
mb.deleted[key] = struct{}{}
|
||||
delete(mb.dirty, key)
|
||||
}
|
||||
|
||||
func (mb *MapBuilder[K, VBase, VBuilder]) Clear() {
|
||||
mb.dirty = make(map[K]VBuilder)
|
||||
mb.deleted = make(map[K]struct{}, len(mb.base))
|
||||
for key := range mb.base {
|
||||
mb.deleted[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (mb *MapBuilder[K, VBase, VBuilder]) Has(key K) bool {
|
||||
if _, ok := mb.deleted[key]; ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := mb.dirty[key]; ok {
|
||||
return true
|
||||
}
|
||||
_, ok := mb.base[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (mb *MapBuilder[K, VBase, VBuilder]) Build() map[K]VBase {
|
||||
if len(mb.dirty) == 0 && len(mb.deleted) == 0 {
|
||||
return mb.base
|
||||
}
|
||||
result := maps.Clone(mb.base)
|
||||
if result == nil {
|
||||
result = make(map[K]VBase)
|
||||
}
|
||||
for key := range mb.deleted {
|
||||
delete(result, key)
|
||||
}
|
||||
for key, value := range mb.dirty {
|
||||
result[key] = mb.build(value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
362
tools/tsgo/internal/project/dirty/syncmap.go
Normal file
362
tools/tsgo/internal/project/dirty/syncmap.go
Normal file
@@ -0,0 +1,362 @@
|
||||
package dirty
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"sync"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/collections"
|
||||
)
|
||||
|
||||
type lockedEntry[K comparable, V Cloneable[V]] struct {
|
||||
e *SyncMapEntry[K, V]
|
||||
}
|
||||
|
||||
func (e *lockedEntry[K, V]) Value() V {
|
||||
return e.e.valueLocked()
|
||||
}
|
||||
|
||||
func (e *lockedEntry[K, V]) Original() V {
|
||||
return e.e.original
|
||||
}
|
||||
|
||||
func (e *lockedEntry[K, V]) Dirty() bool {
|
||||
return e.e.dirty
|
||||
}
|
||||
|
||||
func (e *lockedEntry[K, V]) Change(apply func(V)) {
|
||||
e.e.changeLocked(apply)
|
||||
}
|
||||
|
||||
func (e *lockedEntry[K, V]) ChangeIf(cond func(V) bool, apply func(V)) bool {
|
||||
if cond(e.e.valueLocked()) {
|
||||
e.e.changeLocked(apply)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *lockedEntry[K, V]) Delete() {
|
||||
e.e.deleteLocked()
|
||||
}
|
||||
|
||||
func (e *lockedEntry[K, V]) Locked(fn func(Value[V])) {
|
||||
fn(e)
|
||||
}
|
||||
|
||||
type SyncMapEntry[K comparable, V Cloneable[V]] struct {
|
||||
m *SyncMap[K, V]
|
||||
mu sync.Mutex
|
||||
mapEntry[K, V]
|
||||
// proxyFor is set when this entry loses a race to become the dirty entry
|
||||
// for a value. Since two goroutines hold a reference to two entries that
|
||||
// may try to mutate the same underlying value, all mutations are routed
|
||||
// through the one that actually exists in the dirty map.
|
||||
proxyFor *SyncMapEntry[K, V]
|
||||
}
|
||||
|
||||
func (e *SyncMapEntry[K, V]) Value() V {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.proxyFor != nil {
|
||||
return e.proxyFor.Value()
|
||||
}
|
||||
return e.valueLocked()
|
||||
}
|
||||
|
||||
func (e *SyncMapEntry[K, V]) valueLocked() V {
|
||||
if e.delete {
|
||||
var zero V
|
||||
return zero
|
||||
}
|
||||
return e.value
|
||||
}
|
||||
|
||||
func (e *SyncMapEntry[K, V]) Dirty() bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.proxyFor != nil {
|
||||
return e.proxyFor.Dirty()
|
||||
}
|
||||
return e.dirty
|
||||
}
|
||||
|
||||
func (e *SyncMapEntry[K, V]) Locked(fn func(Value[V])) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.proxyFor != nil {
|
||||
e.proxyFor.Locked(fn)
|
||||
return
|
||||
}
|
||||
fn(&lockedEntry[K, V]{e: e})
|
||||
}
|
||||
|
||||
func (e *SyncMapEntry[K, V]) Change(apply func(V)) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.proxyFor != nil {
|
||||
e.proxyFor.Change(apply)
|
||||
return
|
||||
}
|
||||
e.changeLocked(apply)
|
||||
}
|
||||
|
||||
func (e *SyncMapEntry[K, V]) changeLocked(apply func(V)) {
|
||||
if e.dirty {
|
||||
apply(e.value)
|
||||
return
|
||||
}
|
||||
|
||||
entry, loaded := e.m.dirty.LoadOrStore(e.key, e)
|
||||
if loaded {
|
||||
entry.mu.Lock()
|
||||
defer entry.mu.Unlock()
|
||||
}
|
||||
if !entry.dirty {
|
||||
entry.value = entry.value.Clone()
|
||||
entry.dirty = true
|
||||
}
|
||||
if loaded {
|
||||
e.proxyFor = entry
|
||||
e.value = entry.value
|
||||
e.dirty = true
|
||||
e.delete = entry.delete
|
||||
}
|
||||
apply(entry.value)
|
||||
}
|
||||
|
||||
func (e *SyncMapEntry[K, V]) ChangeIf(cond func(V) bool, apply func(V)) bool {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.proxyFor != nil {
|
||||
return e.proxyFor.ChangeIf(cond, apply)
|
||||
}
|
||||
|
||||
if cond(e.value) {
|
||||
e.changeLocked(apply)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e *SyncMapEntry[K, V]) Delete() {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.proxyFor != nil {
|
||||
e.proxyFor.Delete()
|
||||
return
|
||||
}
|
||||
|
||||
if e.dirty {
|
||||
e.delete = true
|
||||
return
|
||||
}
|
||||
entry, loaded := e.m.dirty.LoadOrStore(e.key, e)
|
||||
if loaded {
|
||||
entry.mu.Lock()
|
||||
defer entry.mu.Unlock()
|
||||
e.delete = true
|
||||
} else {
|
||||
entry.delete = true
|
||||
}
|
||||
}
|
||||
|
||||
func (e *SyncMapEntry[K, V]) deleteLocked() {
|
||||
if e.dirty {
|
||||
e.delete = true
|
||||
return
|
||||
}
|
||||
entry, loaded := e.m.dirty.LoadOrStore(e.key, e)
|
||||
if loaded {
|
||||
entry.mu.Lock()
|
||||
defer entry.mu.Unlock()
|
||||
e.proxyFor = entry
|
||||
e.value = entry.value
|
||||
e.delete = true
|
||||
e.dirty = entry.dirty
|
||||
}
|
||||
entry.delete = true
|
||||
}
|
||||
|
||||
func (e *SyncMapEntry[K, V]) DeleteIf(cond func(V) bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
if e.proxyFor != nil {
|
||||
e.proxyFor.DeleteIf(cond)
|
||||
return
|
||||
}
|
||||
if cond(e.value) {
|
||||
e.deleteLocked()
|
||||
}
|
||||
}
|
||||
|
||||
type SyncMap[K comparable, V Cloneable[V]] struct {
|
||||
base map[K]V
|
||||
dirty collections.SyncMap[K, *SyncMapEntry[K, V]]
|
||||
}
|
||||
|
||||
func NewSyncMap[K comparable, V Cloneable[V]](base map[K]V) *SyncMap[K, V] {
|
||||
return &SyncMap[K, V]{
|
||||
base: base,
|
||||
dirty: collections.SyncMap[K, *SyncMapEntry[K, V]]{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SyncMap[K, V]) Load(key K) (*SyncMapEntry[K, V], bool) {
|
||||
if entry, ok := m.dirty.Load(key); ok {
|
||||
entry.mu.Lock()
|
||||
defer entry.mu.Unlock()
|
||||
if entry.delete {
|
||||
return nil, false
|
||||
}
|
||||
return entry, true
|
||||
}
|
||||
if val, ok := m.base[key]; ok {
|
||||
return &SyncMapEntry[K, V]{
|
||||
m: m,
|
||||
mapEntry: mapEntry[K, V]{
|
||||
key: key,
|
||||
original: val,
|
||||
value: val,
|
||||
dirty: false,
|
||||
delete: false,
|
||||
},
|
||||
}, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (*SyncMapEntry[K, V], bool) {
|
||||
// Check for existence in the base map first so the sync map access is atomic.
|
||||
if baseValue, ok := m.base[key]; ok {
|
||||
if dirty, ok := m.dirty.Load(key); ok {
|
||||
dirty.mu.Lock()
|
||||
defer dirty.mu.Unlock()
|
||||
if dirty.delete {
|
||||
return nil, false
|
||||
}
|
||||
return dirty, true
|
||||
}
|
||||
return &SyncMapEntry[K, V]{
|
||||
m: m,
|
||||
mapEntry: mapEntry[K, V]{
|
||||
key: key,
|
||||
original: baseValue,
|
||||
value: baseValue,
|
||||
dirty: false,
|
||||
delete: false,
|
||||
},
|
||||
}, true
|
||||
}
|
||||
entry, loaded := m.dirty.LoadOrStore(key, &SyncMapEntry[K, V]{
|
||||
m: m,
|
||||
mapEntry: mapEntry[K, V]{
|
||||
key: key,
|
||||
value: value,
|
||||
dirty: true,
|
||||
},
|
||||
})
|
||||
if loaded {
|
||||
entry.mu.Lock()
|
||||
defer entry.mu.Unlock()
|
||||
if entry.delete {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return entry, loaded
|
||||
}
|
||||
|
||||
func (m *SyncMap[K, V]) Delete(key K) {
|
||||
entry, loaded := m.dirty.LoadOrStore(key, &SyncMapEntry[K, V]{
|
||||
m: m,
|
||||
mapEntry: mapEntry[K, V]{
|
||||
key: key,
|
||||
original: m.base[key],
|
||||
delete: true,
|
||||
},
|
||||
})
|
||||
if loaded {
|
||||
entry.Delete()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SyncMap[K, V]) Range(fn func(*SyncMapEntry[K, V]) bool) {
|
||||
seenInDirty := make(map[K]struct{})
|
||||
m.dirty.Range(func(key K, entry *SyncMapEntry[K, V]) bool {
|
||||
seenInDirty[key] = struct{}{}
|
||||
entry.mu.Lock()
|
||||
deleted := entry.delete
|
||||
entry.mu.Unlock()
|
||||
if !deleted && !fn(entry) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
for key, value := range m.base {
|
||||
if _, ok := seenInDirty[key]; ok {
|
||||
continue // already processed in dirty entries
|
||||
}
|
||||
if !fn(&SyncMapEntry[K, V]{m: m, mapEntry: mapEntry[K, V]{
|
||||
key: key,
|
||||
original: value,
|
||||
value: value,
|
||||
dirty: false,
|
||||
}}) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type FinalizationHooks[K comparable, V any] struct {
|
||||
OnDelete func(key K, value V)
|
||||
OnChange func(key K, oldValue V, newValue V)
|
||||
OnAdd func(key K, value V)
|
||||
}
|
||||
|
||||
func (m *SyncMap[K, V]) finalize(hooks FinalizationHooks[K, V]) (map[K]V, bool) {
|
||||
var changed bool
|
||||
result := m.base
|
||||
ensureCloned := func() {
|
||||
if !changed {
|
||||
if m.base == nil {
|
||||
result = make(map[K]V)
|
||||
} else {
|
||||
result = maps.Clone(m.base)
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
m.dirty.Range(func(key K, entry *SyncMapEntry[K, V]) bool {
|
||||
entry.mu.Lock()
|
||||
defer entry.mu.Unlock()
|
||||
if entry.delete {
|
||||
ensureCloned()
|
||||
if hooks.OnDelete != nil {
|
||||
hooks.OnDelete(key, entry.value)
|
||||
}
|
||||
delete(result, key)
|
||||
} else if entry.dirty {
|
||||
ensureCloned()
|
||||
if hooks.OnChange != nil || hooks.OnAdd != nil {
|
||||
if _, ok := m.base[key]; ok {
|
||||
if hooks.OnChange != nil {
|
||||
hooks.OnChange(key, entry.original, entry.value)
|
||||
}
|
||||
} else if hooks.OnAdd != nil {
|
||||
hooks.OnAdd(key, entry.value)
|
||||
}
|
||||
}
|
||||
result[key] = entry.value
|
||||
}
|
||||
return true
|
||||
})
|
||||
return result, changed
|
||||
}
|
||||
|
||||
func (m *SyncMap[K, V]) Finalize() (map[K]V, bool) {
|
||||
return m.finalize(FinalizationHooks[K, V]{})
|
||||
}
|
||||
|
||||
func (m *SyncMap[K, V]) FinalizeWith(hooks FinalizationHooks[K, V]) (map[K]V, bool) {
|
||||
return m.finalize(hooks)
|
||||
}
|
||||
245
tools/tsgo/internal/project/dirty/syncmap_test.go
Normal file
245
tools/tsgo/internal/project/dirty/syncmap_test.go
Normal file
@@ -0,0 +1,245 @@
|
||||
package dirty
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
// testValue is a simple cloneable type for testing
|
||||
type testValue struct {
|
||||
data string
|
||||
}
|
||||
|
||||
func (v *testValue) Clone() *testValue {
|
||||
return &testValue{data: v.data}
|
||||
}
|
||||
|
||||
func TestSyncMapProxyFor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("proxy for race condition", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a sync map with a base value
|
||||
base := map[string]*testValue{
|
||||
"key1": {data: "original"},
|
||||
}
|
||||
syncMap := NewSyncMap(base)
|
||||
|
||||
// Load the same entry from multiple goroutines to simulate race condition
|
||||
var entry1, entry2 *SyncMapEntry[string, *testValue]
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
|
||||
// First goroutine loads the entry
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var ok bool
|
||||
entry1, ok = syncMap.Load("key1")
|
||||
assert.Assert(t, ok, "entry1 should be loaded")
|
||||
}()
|
||||
|
||||
// Second goroutine loads the same entry
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var ok bool
|
||||
entry2, ok = syncMap.Load("key1")
|
||||
assert.Assert(t, ok, "entry2 should be loaded")
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Both entries should exist and have the same initial value
|
||||
assert.Equal(t, "original", entry1.Value().data)
|
||||
assert.Equal(t, "original", entry2.Value().data)
|
||||
assert.Equal(t, false, entry1.Dirty())
|
||||
assert.Equal(t, false, entry2.Dirty())
|
||||
|
||||
// Now try to change both entries concurrently to trigger the proxy mechanism.
|
||||
// (This change doesn't actually have to be concurrent to test the proxy behavior,
|
||||
// but might exercise concurrency safety in -race mode.)
|
||||
var changeWg sync.WaitGroup
|
||||
changeWg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer changeWg.Done()
|
||||
entry1.Change(func(v *testValue) {
|
||||
v.data = "changed_by_entry1"
|
||||
})
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer changeWg.Done()
|
||||
entry2.Change(func(v *testValue) {
|
||||
v.data = "changed_by_entry2"
|
||||
})
|
||||
}()
|
||||
|
||||
changeWg.Wait()
|
||||
|
||||
// After the race, one entry should have proxyFor set and both should reflect the same final state
|
||||
// The exact final value depends on which goroutine wins the race, but both entries should be consistent
|
||||
finalValue1 := entry1.Value().data
|
||||
finalValue2 := entry2.Value().data
|
||||
assert.Equal(t, finalValue1, finalValue2, "both entries should have the same final value")
|
||||
|
||||
// Both entries should be marked as dirty
|
||||
assert.Equal(t, true, entry1.Dirty())
|
||||
assert.Equal(t, true, entry2.Dirty())
|
||||
|
||||
// At least one entry should have proxyFor set (the one that lost the race)
|
||||
hasProxy := (entry1.proxyFor != nil) || (entry2.proxyFor != nil)
|
||||
assert.Assert(t, hasProxy, "at least one entry should have proxyFor set")
|
||||
|
||||
// If entry1 has a proxy, it should point to entry2, and vice versa
|
||||
if entry1.proxyFor != nil {
|
||||
assert.Equal(t, entry2, entry1.proxyFor, "entry1 should proxy to entry2")
|
||||
}
|
||||
if entry2.proxyFor != nil {
|
||||
assert.Equal(t, entry1, entry2.proxyFor, "entry2 should proxy to entry1")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("proxy operations delegation", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := map[string]*testValue{
|
||||
"key1": {data: "original"},
|
||||
}
|
||||
syncMap := NewSyncMap(base)
|
||||
|
||||
// Load two entries for the same key
|
||||
entry1, ok1 := syncMap.Load("key1")
|
||||
assert.Assert(t, ok1)
|
||||
entry2, ok2 := syncMap.Load("key1")
|
||||
assert.Assert(t, ok2)
|
||||
|
||||
// Force one to become a proxy by making them both dirty in sequence
|
||||
entry1.Change(func(v *testValue) {
|
||||
v.data = "changed_by_entry1"
|
||||
})
|
||||
entry2.Change(func(v *testValue) {
|
||||
v.data = "changed_by_entry2"
|
||||
})
|
||||
|
||||
// Determine which is the proxy and which is the target
|
||||
var proxy, target *SyncMapEntry[string, *testValue]
|
||||
if entry1.proxyFor != nil {
|
||||
proxy = entry1
|
||||
target = entry2
|
||||
} else {
|
||||
proxy = entry2
|
||||
target = entry1
|
||||
}
|
||||
|
||||
// Test that proxy operations are delegated to the target
|
||||
// Change through proxy should affect target
|
||||
proxy.Change(func(v *testValue) {
|
||||
v.data = "changed_through_proxy"
|
||||
})
|
||||
assert.Equal(t, "changed_through_proxy", target.Value().data)
|
||||
assert.Equal(t, "changed_through_proxy", proxy.Value().data)
|
||||
|
||||
// ChangeIf through proxy should work
|
||||
changed := proxy.ChangeIf(
|
||||
func(v *testValue) bool { return v.data == "changed_through_proxy" },
|
||||
func(v *testValue) { v.data = "conditional_change" },
|
||||
)
|
||||
assert.Assert(t, changed)
|
||||
assert.Equal(t, "conditional_change", target.Value().data)
|
||||
assert.Equal(t, "conditional_change", proxy.Value().data)
|
||||
|
||||
// Dirty status should be consistent
|
||||
assert.Equal(t, target.Dirty(), proxy.Dirty())
|
||||
|
||||
// Locked operations should work through proxy
|
||||
proxy.Locked(func(v Value[*testValue]) {
|
||||
v.Change(func(val *testValue) {
|
||||
val.data = "locked_change"
|
||||
})
|
||||
})
|
||||
assert.Equal(t, "locked_change", target.Value().data)
|
||||
assert.Equal(t, "locked_change", proxy.Value().data)
|
||||
})
|
||||
|
||||
t.Run("proxy delete operations", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := map[string]*testValue{
|
||||
"key1": {data: "original"},
|
||||
}
|
||||
syncMap := NewSyncMap(base)
|
||||
|
||||
// Load two entries and make one a proxy
|
||||
entry1, _ := syncMap.Load("key1")
|
||||
entry2, _ := syncMap.Load("key1")
|
||||
|
||||
entry1.Change(func(v *testValue) { v.data = "modified" })
|
||||
entry2.Change(func(v *testValue) { v.data = "modified2" })
|
||||
|
||||
// Determine which is the proxy
|
||||
var proxy *SyncMapEntry[string, *testValue]
|
||||
if entry1.proxyFor != nil {
|
||||
proxy = entry1
|
||||
} else {
|
||||
proxy = entry2
|
||||
}
|
||||
|
||||
// Delete through proxy should affect target
|
||||
proxy.Delete()
|
||||
|
||||
// Both should reflect the deletion
|
||||
_, exists := syncMap.Load("key1")
|
||||
assert.Equal(t, false, exists, "key should be deleted from sync map")
|
||||
|
||||
// DeleteIf through proxy should work
|
||||
base2 := map[string]*testValue{
|
||||
"key2": {data: "test"},
|
||||
}
|
||||
syncMap2 := NewSyncMap(base2)
|
||||
|
||||
entry3, _ := syncMap2.Load("key2")
|
||||
entry4, _ := syncMap2.Load("key2")
|
||||
|
||||
entry3.Change(func(v *testValue) { v.data = "modified" })
|
||||
entry4.Change(func(v *testValue) { v.data = "modified2" })
|
||||
|
||||
var proxy2 *SyncMapEntry[string, *testValue]
|
||||
if entry3.proxyFor != nil {
|
||||
proxy2 = entry3
|
||||
} else {
|
||||
proxy2 = entry4
|
||||
}
|
||||
|
||||
proxy2.DeleteIf(func(v *testValue) bool {
|
||||
return v.data == "modified2" || v.data == "modified"
|
||||
})
|
||||
|
||||
_, exists2 := syncMap2.Load("key2")
|
||||
assert.Equal(t, false, exists2, "key2 should be deleted conditionally")
|
||||
})
|
||||
|
||||
t.Run("no proxy when no race", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := map[string]*testValue{
|
||||
"key1": {data: "original"},
|
||||
}
|
||||
syncMap := NewSyncMap(base)
|
||||
|
||||
// Load and modify a single entry - no race condition
|
||||
entry, ok := syncMap.Load("key1")
|
||||
assert.Assert(t, ok)
|
||||
|
||||
entry.Change(func(v *testValue) {
|
||||
v.data = "changed"
|
||||
})
|
||||
|
||||
// Should not have a proxy since there was no race
|
||||
assert.Assert(t, entry.proxyFor == nil, "entry should not have proxyFor when no race occurs")
|
||||
assert.Equal(t, true, entry.Dirty())
|
||||
assert.Equal(t, "changed", entry.Value().data)
|
||||
})
|
||||
}
|
||||
18
tools/tsgo/internal/project/dirty/util.go
Normal file
18
tools/tsgo/internal/project/dirty/util.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package dirty
|
||||
|
||||
import "maps"
|
||||
|
||||
func CloneMapIfNil[K comparable, V any, T any](dirty *T, original *T, getMap func(*T) map[K]V) map[K]V {
|
||||
dirtyMap := getMap(dirty)
|
||||
if dirtyMap == nil {
|
||||
if original == nil {
|
||||
return make(map[K]V)
|
||||
}
|
||||
originalMap := getMap(original)
|
||||
if originalMap == nil {
|
||||
return make(map[K]V)
|
||||
}
|
||||
return maps.Clone(originalMap)
|
||||
}
|
||||
return dirtyMap
|
||||
}
|
||||
Reference in New Issue
Block a user