github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/mod-check-default.go (about)

     1  //go:build !tinygo
     2  // +build !tinygo
     3  
     4  package vugu
     5  
     6  import (
     7  	"bytes"
     8  	"errors"
     9  	"fmt"
    10  	"reflect"
    11  	"strings"
    12  	"unsafe"
    13  
    14  	"github.com/vugu/xxhash"
    15  )
    16  
    17  // type ModCheckedString string
    18  // func (s ModCheckedString) ModCheck(mt *ModTracker, oldData interface{}) (isModified bool, newData interface{}) {
    19  // }
    20  
    21  // ModTracker tracks modifications and maintains the appropriate state for this.
    22  type ModTracker struct {
    23  	old map[interface{}]mtResult
    24  	cur map[interface{}]mtResult
    25  }
    26  
    27  // TrackNext moves the "current" information to the "old" position - starting a new round of change tracking.
    28  // Calls to ModCheckAll after calling TrackNext will compare current values to the values from before the call to TrackNext.
    29  // It is generally called once at the start of each build and render cycle.  This method must be called at least
    30  // once before doing modification checks.
    31  func (mt *ModTracker) TrackNext() {
    32  
    33  	// lazy initialize
    34  	if mt.old == nil {
    35  		mt.old = make(map[interface{}]mtResult)
    36  	}
    37  	if mt.cur == nil {
    38  		mt.cur = make(map[interface{}]mtResult)
    39  	}
    40  
    41  	// remove all elements from old map
    42  	for k := range mt.old {
    43  		delete(mt.old, k)
    44  	}
    45  
    46  	// and swap them
    47  	mt.old, mt.cur = mt.cur, mt.old
    48  
    49  }
    50  
    51  //nolint:golint,unused
    52  func (mt *ModTracker) dump() []byte {
    53  	var buf bytes.Buffer
    54  	fmt.Fprintf(&buf, "-- cur (len=%d): --\n", len(mt.cur))
    55  	for k, v := range mt.cur {
    56  		fmt.Fprintf(&buf, " %#v = %#v\n", k, v)
    57  	}
    58  	fmt.Fprintf(&buf, "-- old (len=%d): --\n", len(mt.old))
    59  	for k, v := range mt.old {
    60  		fmt.Fprintf(&buf, " %#v = %#v\n", k, v)
    61  	}
    62  	return buf.Bytes()
    63  }
    64  
    65  // Otherwise pointers to some built-in types are supported including all primitive single-value types -
    66  // bool, int/uint and all variations, both float types, both complex types, string.
    67  // Pointers to supported types are supported.
    68  // Arrays, slices and maps(nope!) using supported types are supported.
    69  // Pointers to structs will be checked by
    70  // checking each field with a struct tag like `vugu:"modcheck"`.  Slices and arrays of
    71  // structs are okay, since their members have a stable position in memory and a pointer
    72  // can be taken.  Maps using structs however must use pointers to them
    73  // (restriction applies to both keys and values) to be supported.
    74  
    75  // ModCheckAll performs a modification check on the values provided.
    76  // For values implementing the ModChecker interface, the ModCheck method will be called.
    77  // All values passed should be pointers to the types described below.
    78  // Single-value primitive types are supported.  Structs are supported and
    79  // and are traversed by calling ModCheckAll on each with the tag `vugu:"modcheck"`.
    80  // Arrays and slices of supported types are supported, their length is compared as well
    81  // as a pointer to each member.
    82  // As a special case []byte is treated like a string.
    83  // Maps are not supported at this time.
    84  // Other weird and wonderful things like channels and funcs are not supported.
    85  // Passing an unsupported type will result in a panic.
    86  func (mt *ModTracker) ModCheckAll(values ...interface{}) (ret bool) {
    87  
    88  	for _, v := range values {
    89  
    90  		// check if we've already done a mod check on v
    91  		curres, ok := mt.cur[v]
    92  		if ok {
    93  			ret = ret || curres.modified
    94  			continue
    95  		}
    96  
    97  		// look up the old data
    98  		oldres := mt.old[v]
    99  
   100  		// the result of the mod check on v goes here
   101  		var mod bool
   102  		var newdata interface{}
   103  
   104  		{
   105  			// see if it implements the ModChecker interface
   106  			mc, ok := v.(ModChecker)
   107  			if ok {
   108  				mod, newdata = mc.ModCheck(mt, oldres.data)
   109  				goto handleData
   110  			}
   111  
   112  			// support for certain built-in types
   113  			switch vt := v.(type) {
   114  
   115  			case *string:
   116  				oldval, ok := oldres.data.(string)
   117  				mod = !ok || oldval != *vt
   118  				newdata = *vt
   119  				goto handleData
   120  
   121  			case *[]byte: // special case of []byte, handled like string not slice
   122  				oldval, ok := oldres.data.(string)
   123  				vts := string(*vt)
   124  				mod = !ok || oldval != vts
   125  				newdata = vts
   126  				goto handleData
   127  
   128  			case *bool:
   129  				oldval, ok := oldres.data.(bool)
   130  				mod = !ok || oldval != *vt
   131  				newdata = *vt
   132  				goto handleData
   133  
   134  			case *int:
   135  				oldval, ok := oldres.data.(int)
   136  				mod = !ok || oldval != *vt
   137  				newdata = *vt
   138  				goto handleData
   139  
   140  			case *int8:
   141  				oldval, ok := oldres.data.(int8)
   142  				mod = !ok || oldval != *vt
   143  				newdata = *vt
   144  				goto handleData
   145  
   146  			case *int16:
   147  				oldval, ok := oldres.data.(int16)
   148  				mod = !ok || oldval != *vt
   149  				newdata = *vt
   150  				goto handleData
   151  
   152  			case *int32:
   153  				oldval, ok := oldres.data.(int32)
   154  				mod = !ok || oldval != *vt
   155  				newdata = *vt
   156  				goto handleData
   157  
   158  			case *int64:
   159  				oldval, ok := oldres.data.(int64)
   160  				mod = !ok || oldval != *vt
   161  				newdata = *vt
   162  				goto handleData
   163  
   164  			case *uint:
   165  				oldval, ok := oldres.data.(uint)
   166  				mod = !ok || oldval != *vt
   167  				newdata = *vt
   168  				goto handleData
   169  
   170  			case *uint8:
   171  				oldval, ok := oldres.data.(uint8)
   172  				mod = !ok || oldval != *vt
   173  				newdata = *vt
   174  				goto handleData
   175  
   176  			case *uint16:
   177  				oldval, ok := oldres.data.(uint16)
   178  				mod = !ok || oldval != *vt
   179  				newdata = *vt
   180  				goto handleData
   181  
   182  			case *uint32:
   183  				oldval, ok := oldres.data.(uint32)
   184  				mod = !ok || oldval != *vt
   185  				newdata = *vt
   186  				goto handleData
   187  
   188  			case *uint64:
   189  				oldval, ok := oldres.data.(uint64)
   190  				mod = !ok || oldval != *vt
   191  				newdata = *vt
   192  				goto handleData
   193  
   194  			case *float32:
   195  				oldval, ok := oldres.data.(float32)
   196  				mod = !ok || oldval != *vt
   197  				newdata = *vt
   198  				goto handleData
   199  
   200  			case *float64:
   201  				oldval, ok := oldres.data.(float64)
   202  				mod = !ok || oldval != *vt
   203  				newdata = *vt
   204  				goto handleData
   205  
   206  			case *complex64:
   207  				oldval, ok := oldres.data.(complex64)
   208  				mod = !ok || oldval != *vt
   209  				newdata = *vt
   210  				goto handleData
   211  
   212  			case *complex128:
   213  				oldval, ok := oldres.data.(complex128)
   214  				mod = !ok || oldval != *vt
   215  				newdata = *vt
   216  				goto handleData
   217  
   218  			}
   219  
   220  			// when the scalpel (type switch) doesn't do it,
   221  			// gotta use the bonesaw (reflection)
   222  
   223  			rv := reflect.ValueOf(v)
   224  
   225  			// check pointer and deref
   226  			if rv.Kind() != reflect.Ptr {
   227  				panic(errors.New("type not implemented (pointer required): " + rv.String()))
   228  			}
   229  			rvv := rv.Elem()
   230  
   231  			// slice and array are treated the same
   232  			if rvv.Kind() == reflect.Slice || rvv.Kind() == reflect.Array {
   233  
   234  				l := rvv.Len()
   235  
   236  				// for slices and arrays we compute the hash of the raw contents,
   237  				// takes care of length and sequence changes
   238  				ha := xxhash.New()
   239  
   240  				var el0t reflect.Type
   241  
   242  				if l > 0 {
   243  					// use the unsafe package to make a byte slice corresponding to the raw slice contents
   244  					var bs []byte
   245  					// TODO: need to be fix later
   246  					bsh := (*reflect.SliceHeader)(unsafe.Pointer(&bs)) //nolint:staticcheck
   247  
   248  					el0 := rvv.Index(0)
   249  					el0t = el0.Type()
   250  
   251  					bsh.Data = el0.Addr().Pointer() // point to first element of slice
   252  					bsh.Len = l * int(el0t.Size())
   253  					bsh.Cap = bsh.Len
   254  
   255  					// hash it
   256  					_, err := ha.Write(bs)
   257  					if err != nil {
   258  						panic(err)
   259  					}
   260  				}
   261  
   262  				hashval := ha.Sum64()
   263  
   264  				// use hashval as our data, and mark as modified if different
   265  				oldval, ok := oldres.data.(uint64)
   266  				mod = !ok || oldval != hashval
   267  				newdata = hashval
   268  
   269  				// for types that by definition have already been checked with the hash above, we're done
   270  				if el0t != nil {
   271  					switch el0t.Kind() {
   272  					case reflect.Bool,
   273  						reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
   274  						reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
   275  						reflect.Uintptr,
   276  						reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128,
   277  						reflect.String:
   278  						goto handleData
   279  					}
   280  				}
   281  
   282  				// recurse into each element and check, update mod as we go
   283  				// NOTE: for "deep" element types that can have changes within them,
   284  				// it's important to recurse into children even if mod is already
   285  				// true, otherwise we'll never call ModCheckAll on these children and
   286  				// never get an unmodified response
   287  
   288  				for i := 0; i < l; i++ {
   289  
   290  					// get pointer to the individual element and recurse into it
   291  					elv := rvv.Index(i).Addr().Interface()
   292  					mod = mt.ModCheckAll(elv) || mod
   293  				}
   294  
   295  				goto handleData
   296  
   297  			}
   298  
   299  			// for structs we iterate over the fields looked for tagged ones
   300  			if rvv.Kind() == reflect.Struct {
   301  
   302  				// just use bool(true) as the data value for a struct - this way it will always be modified the first time,
   303  				// but every subsequent check will solely depend on the checks on it's fields
   304  				oldval, ok := oldres.data.(bool)
   305  				mod = !ok || !oldval
   306  				newdata = true
   307  
   308  				rvvt := rvv.Type()
   309  				for i := 0; i < rvvt.NumField(); i++ {
   310  					// skip untagged fields
   311  					if !hasTagPart(rvvt.Field(i).Tag.Get("vugu"), "data") {
   312  						continue
   313  					}
   314  
   315  					// call ModCheckAll on pointer to field
   316  					mod = mt.ModCheckAll(
   317  						rvv.Field(i).Addr().Interface(),
   318  					) || mod
   319  
   320  				}
   321  
   322  				goto handleData
   323  			}
   324  
   325  			// random stream of conciousness: should we check for a ModCheck implementation here and call it?
   326  			// We might want to follow these pointers or rather recurse into them (if not nil?)
   327  			// with a ModCheckAll.  Also look at how this ties into maps (if at all), since theoretically
   328  			// we could compare a map by storing its length and basically doing for k, v := range m { ...ModCheckAll(&k,&v)... }
   329  			// actually no that won't work because k and v will have different locations each time - but still could
   330  			// potentially make some mapKeyValue struct that does what we need - but not vital to solve right now.
   331  			// Pointers to pointers will probably come up first, and is likely why you are reading this comment.
   332  
   333  			// pointer (meaning we were originally passed a pointer to a pointer)
   334  			if rvv.Kind() == reflect.Ptr {
   335  
   336  				// use pointer value as data...
   337  				vv := rvv.Pointer()
   338  				oldval, ok := oldres.data.(uintptr)
   339  				mod = !ok || oldval != vv
   340  				newdata = vv
   341  
   342  				// ...but also recurse and call ModChecker with one level of pointer dereferencing, if not nil
   343  				if !rvv.IsNil() {
   344  					mod = mt.ModCheckAll(
   345  						rvv.Interface(),
   346  					) || mod
   347  				}
   348  
   349  				goto handleData
   350  			}
   351  
   352  			panic(errors.New("type not implemented: " + reflect.TypeOf(v).String()))
   353  
   354  		}
   355  	handleData:
   356  		mt.cur[v] = mtResult{modified: mod, data: newdata}
   357  		ret = ret || mod
   358  
   359  	}
   360  
   361  	return ret
   362  }
   363  
   364  // ModCheck(oldData interface{}) (isModified bool, newData interface{})
   365  
   366  // hm, this may not work - what happens if we call ModCheck twice in a row?? we need a clear
   367  // way to demark the "old" and "new" versions of data - it might be that the comparison and
   368  // the gather of the new value need to be separate methods?  Or can ModTracker somehow
   369  // prevent ModCheck from being call twice in the same pass (or prevent that from being an issue)
   370  
   371  // actually that might work - if there is a call on ModTracker that moves "new" to "old", it
   372  // woudl be pretty clear - and then calling this would only update the "new" value.  Also the
   373  // presence of something in the "new" data would mean it's already been called in this pass
   374  // and so can be deduplicated.
   375  
   376  func hasTagPart(tagstr, part string) bool {
   377  	for _, p := range strings.Split(tagstr, ",") {
   378  		if p == part {
   379  			return true
   380  		}
   381  	}
   382  	return false
   383  }