From f4d24ddbbac8979d08a2e5b81e9b017bf8394510 Mon Sep 17 00:00:00 2001 From: Andrei Mihu Date: Wed, 17 Aug 2022 23:17:06 +0100 Subject: [PATCH] Better concurrent map structure, specify Go 1.18 in mod file for generics support. --- go.mod | 2 +- go.sum | 10 +- server/map.go | 389 ++++++++++++++++++ server/match_registry.go | 44 +- server/party_registry.go | 28 +- server/runtime_lua.go | 18 +- server/session_registry.go | 11 +- .../dlclark/regexp2/syntax/parser.go | 2 +- vendor/github.com/dop251/goja/ast/node.go | 3 +- vendor/github.com/dop251/goja/parser/error.go | 1 + vendor/github.com/dop251/goja/runtime.go | 209 +++++----- vendor/github.com/dop251/goja/token/token.go | 32 +- 12 files changed, 565 insertions(+), 184 deletions(-) create mode 100644 server/map.go diff --git a/go.mod b/go.mod index 8969d24bb..84cf29473 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/heroiclabs/nakama/v3 -go 1.17 +go 1.18 require ( github.com/blugelabs/bluge v0.1.9 diff --git a/go.sum b/go.sum index 154a85887..d8a253bfc 100644 --- a/go.sum +++ b/go.sum @@ -100,7 +100,6 @@ github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go. github.com/caio/go-tdigest v3.1.0+incompatible h1:uoVMJ3Q5lXmVLCCqaMGHLBWnbGoN6Lpu7OAUPR60cds= github.com/caio/go-tdigest v3.1.0+incompatible/go.mod h1:sHQM/ubZStBUmF1WbB8FAm8q9GjDajLC5T7ydxE3JHI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= @@ -136,13 +135,10 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8= github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= -github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= -github.com/dop251/goja v0.0.0-20220403115030-90825c02072b h1:W/gxdQhJQDARfor8C7DASS0NhcFDzAZmL+XkAEgwnRI= -github.com/dop251/goja v0.0.0-20220403115030-90825c02072b/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20220806120448-1444e6b94559 h1:S3U65m9SN2p5CJpT3CDuqhN+rNJZXDoABYPKdQ7DOfY= github.com/dop251/goja v0.0.0-20220806120448-1444e6b94559/go.mod h1:1jWwHOtOkEqsfX6tYsufUc7BBTuGHH2ekiJabpkN4CA= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= @@ -272,7 +268,6 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= @@ -304,7 +299,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1: github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb v1.7.6/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY= -github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= @@ -327,7 +321,6 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= @@ -374,8 +367,8 @@ github.com/kortschak/utter v1.0.1/go.mod h1:vSmSjbyrlKjjsL71193LmzBOKgwePk9DH6uF github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= @@ -472,6 +465,7 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= diff --git a/server/map.go b/server/map.go new file mode 100644 index 000000000..2245bba4f --- /dev/null +++ b/server/map.go @@ -0,0 +1,389 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Retrieved from: https://github.com/SaveTheRbtz/generic-sync-map-go +// August 10th, 2022 at 22:00 UTC +// BSD license derived from Go standard library sync.Map source. + +package server + +import ( + "sync" + "sync/atomic" + "unsafe" +) + +// MapOf is like a Go map[interface{}]interface{} but is safe for concurrent use +// by multiple goroutines without additional locking or coordination. +// Loads, stores, and deletes run in amortized constant time. +// +// The MapOf type is specialized. Most code should use a plain Go map instead, +// with separate locking or coordination, for better type safety and to make it +// easier to maintain other invariants along with the map content. +// +// The MapOf type is optimized for two common use cases: (1) when the entry for a given +// key is only ever written once but read many times, as in caches that only grow, +// or (2) when multiple goroutines read, write, and overwrite entries for disjoint +// sets of keys. In these two cases, use of a MapOf may significantly reduce lock +// contention compared to a Go map paired with a separate Mutex or RWMutex. +// +// The zero MapOf is empty and ready for use. A MapOf must not be copied after first use. +type MapOf[K comparable, V any] struct { + mu sync.Mutex + + // read contains the portion of the map's contents that are safe for + // concurrent access (with or without mu held). + // + // The read field itself is always safe to load, but must only be stored with + // mu held. + // + // Entries stored in read may be updated concurrently without mu, but updating + // a previously-expunged entry requires that the entry be copied to the dirty + // map and unexpunged with mu held. + read atomic.Value // readOnly + + // dirty contains the portion of the map's contents that require mu to be + // held. To ensure that the dirty map can be promoted to the read map quickly, + // it also includes all of the non-expunged entries in the read map. + // + // Expunged entries are not stored in the dirty map. An expunged entry in the + // clean map must be unexpunged and added to the dirty map before a new value + // can be stored to it. + // + // If the dirty map is nil, the next write to the map will initialize it by + // making a shallow copy of the clean map, omitting stale entries. + dirty map[K]*entry[V] + + // misses counts the number of loads since the read map was last updated that + // needed to lock mu to determine whether the key was present. + // + // Once enough misses have occurred to cover the cost of copying the dirty + // map, the dirty map will be promoted to the read map (in the unamended + // state) and the next store to the map will make a new dirty copy. + misses int +} + +// readOnly is an immutable struct stored atomically in the MapOf.read field. +type readOnly[K comparable, V any] struct { + m map[K]*entry[V] + amended bool // true if the dirty map contains some key not in m. +} + +// expunged is an arbitrary pointer that marks entries which have been deleted +// from the dirty map. +var expunged = unsafe.Pointer(new(interface{})) + +// An entry is a slot in the map corresponding to a particular key. +type entry[V any] struct { + // p points to the interface{} value stored for the entry. + // + // If p == nil, the entry has been deleted and m.dirty == nil. + // + // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry + // is missing from m.dirty. + // + // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty + // != nil, in m.dirty[key]. + // + // An entry can be deleted by atomic replacement with nil: when m.dirty is + // next created, it will atomically replace nil with expunged and leave + // m.dirty[key] unset. + // + // An entry's associated value can be updated by atomic replacement, provided + // p != expunged. If p == expunged, an entry's associated value can be updated + // only after first setting m.dirty[key] = e so that lookups using the dirty + // map find the entry. + p unsafe.Pointer // *interface{} +} + +func newEntry[V any](i V) *entry[V] { + return &entry[V]{p: unsafe.Pointer(&i)} +} + +// Load returns the value stored in the map for a key, or nil if no +// value is present. +// The ok result indicates whether value was found in the map. +func (m *MapOf[K, V]) Load(key K) (value V, ok bool) { + read, _ := m.read.Load().(readOnly[K, V]) + e, ok := read.m[key] + if !ok && read.amended { + m.mu.Lock() + // Avoid reporting a spurious miss if m.dirty got promoted while we were + // blocked on m.mu. (If further loads of the same key will not miss, it's + // not worth copying the dirty map for this key.) + read, _ = m.read.Load().(readOnly[K, V]) + e, ok = read.m[key] + if !ok && read.amended { + e, ok = m.dirty[key] + // Regardless of whether the entry was present, record a miss: this key + // will take the slow path until the dirty map is promoted to the read + // map. + m.missLocked() + } + m.mu.Unlock() + } + if !ok { + return value, false + } + return e.load() +} + +func (e *entry[V]) load() (value V, ok bool) { + p := atomic.LoadPointer(&e.p) + if p == nil || p == expunged { + return value, false + } + return *(*V)(p), true +} + +// Store sets the value for a key. +func (m *MapOf[K, V]) Store(key K, value V) { + read, _ := m.read.Load().(readOnly[K, V]) + if e, ok := read.m[key]; ok && e.tryStore(&value) { + return + } + + m.mu.Lock() + read, _ = m.read.Load().(readOnly[K, V]) + if e, ok := read.m[key]; ok { + if e.unexpungeLocked() { + // The entry was previously expunged, which implies that there is a + // non-nil dirty map and this entry is not in it. + m.dirty[key] = e + } + e.storeLocked(&value) + } else if e, ok := m.dirty[key]; ok { + e.storeLocked(&value) + } else { + if !read.amended { + // We're adding the first new key to the dirty map. + // Make sure it is allocated and mark the read-only map as incomplete. + m.dirtyLocked() + m.read.Store(readOnly[K, V]{m: read.m, amended: true}) + } + m.dirty[key] = newEntry(value) + } + m.mu.Unlock() +} + +// tryStore stores a value if the entry has not been expunged. +// +// If the entry is expunged, tryStore returns false and leaves the entry +// unchanged. +func (e *entry[V]) tryStore(i *V) bool { + for { + p := atomic.LoadPointer(&e.p) + if p == expunged { + return false + } + if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) { + return true + } + } +} + +// unexpungeLocked ensures that the entry is not marked as expunged. +// +// If the entry was previously expunged, it must be added to the dirty map +// before m.mu is unlocked. +func (e *entry[V]) unexpungeLocked() (wasExpunged bool) { + return atomic.CompareAndSwapPointer(&e.p, expunged, nil) +} + +// storeLocked unconditionally stores a value to the entry. +// +// The entry must be known not to be expunged. +func (e *entry[V]) storeLocked(i *V) { + atomic.StorePointer(&e.p, unsafe.Pointer(i)) +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (m *MapOf[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + // Avoid locking if it's a clean hit. + read, _ := m.read.Load().(readOnly[K, V]) + if e, ok := read.m[key]; ok { + actual, loaded, ok := e.tryLoadOrStore(value) + if ok { + return actual, loaded + } + } + + m.mu.Lock() + read, _ = m.read.Load().(readOnly[K, V]) + if e, ok := read.m[key]; ok { + if e.unexpungeLocked() { + m.dirty[key] = e + } + actual, loaded, _ = e.tryLoadOrStore(value) + } else if e, ok := m.dirty[key]; ok { + actual, loaded, _ = e.tryLoadOrStore(value) + m.missLocked() + } else { + if !read.amended { + // We're adding the first new key to the dirty map. + // Make sure it is allocated and mark the read-only map as incomplete. + m.dirtyLocked() + m.read.Store(readOnly[K, V]{m: read.m, amended: true}) + } + m.dirty[key] = newEntry(value) + actual, loaded = value, false + } + m.mu.Unlock() + + return actual, loaded +} + +// tryLoadOrStore atomically loads or stores a value if the entry is not +// expunged. +// +// If the entry is expunged, tryLoadOrStore leaves the entry unchanged and +// returns with ok==false. +func (e *entry[V]) tryLoadOrStore(i V) (actual V, loaded, ok bool) { + p := atomic.LoadPointer(&e.p) + if p == expunged { + return actual, false, false + } + if p != nil { + return *(*V)(p), true, true + } + + // Copy the interface after the first load to make this method more amenable + // to escape analysis: if we hit the "load" path or the entry is expunged, we + // shouldn't bother heap-allocating. + ic := i + for { + if atomic.CompareAndSwapPointer(&e.p, nil, unsafe.Pointer(&ic)) { + return i, false, true + } + p = atomic.LoadPointer(&e.p) + if p == expunged { + return actual, false, false + } + if p != nil { + return *(*V)(p), true, true + } + } +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +// The loaded result reports whether the key was present. +func (m *MapOf[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + read, _ := m.read.Load().(readOnly[K, V]) + e, ok := read.m[key] + if !ok && read.amended { + m.mu.Lock() + read, _ = m.read.Load().(readOnly[K, V]) + e, ok = read.m[key] + if !ok && read.amended { + e, ok = m.dirty[key] + delete(m.dirty, key) + // Regardless of whether the entry was present, record a miss: this key + // will take the slow path until the dirty map is promoted to the read + // map. + m.missLocked() + } + m.mu.Unlock() + } + if ok { + return e.delete() + } + return value, false +} + +// Delete deletes the value for a key. +func (m *MapOf[K, V]) Delete(key K) { + m.LoadAndDelete(key) +} + +func (e *entry[V]) delete() (value V, ok bool) { + for { + p := atomic.LoadPointer(&e.p) + if p == nil || p == expunged { + return value, false + } + if atomic.CompareAndSwapPointer(&e.p, p, nil) { + return *(*V)(p), true + } + } +} + +// Range calls f sequentially for each key and value present in the map. +// If f returns false, range stops the iteration. +// +// Range does not necessarily correspond to any consistent snapshot of the MapOf's +// contents: no key will be visited more than once, but if the value for any key +// is stored or deleted concurrently, Range may reflect any mapping for that key +// from any point during the Range call. +// +// Range may be O(N) with the number of elements in the map even if f returns +// false after a constant number of calls. +func (m *MapOf[K, V]) Range(f func(key K, value V) bool) { + // We need to be able to iterate over all of the keys that were already + // present at the start of the call to Range. + // If read.amended is false, then read.m satisfies that property without + // requiring us to hold m.mu for a long time. + read, _ := m.read.Load().(readOnly[K, V]) + if read.amended { + // m.dirty contains keys not in read.m. Fortunately, Range is already O(N) + // (assuming the caller does not break out early), so a call to Range + // amortizes an entire copy of the map: we can promote the dirty copy + // immediately! + m.mu.Lock() + read, _ = m.read.Load().(readOnly[K, V]) + if read.amended { + read = readOnly[K, V]{m: m.dirty} + m.read.Store(read) + m.dirty = nil + m.misses = 0 + } + m.mu.Unlock() + } + + for k, e := range read.m { + v, ok := e.load() + if !ok { + continue + } + if !f(k, v) { + break + } + } +} + +func (m *MapOf[K, V]) missLocked() { + m.misses++ + if m.misses < len(m.dirty) { + return + } + m.read.Store(readOnly[K, V]{m: m.dirty}) + m.dirty = nil + m.misses = 0 +} + +func (m *MapOf[K, V]) dirtyLocked() { + if m.dirty != nil { + return + } + + read, _ := m.read.Load().(readOnly[K, V]) + m.dirty = make(map[K]*entry[V], len(read.m)) + for k, e := range read.m { + if !e.tryExpungeLocked() { + m.dirty[k] = e + } + } +} + +func (e *entry[V]) tryExpungeLocked() (isExpunged bool) { + p := atomic.LoadPointer(&e.p) + for p == nil { + if atomic.CompareAndSwapPointer(&e.p, nil, expunged) { + return true + } + p = atomic.LoadPointer(&e.p) + } + return p == expunged +} diff --git a/server/match_registry.go b/server/match_registry.go index 4944c977f..410f053bc 100644 --- a/server/match_registry.go +++ b/server/match_registry.go @@ -135,7 +135,7 @@ type LocalMatchRegistry struct { ctx context.Context ctxCancelFn context.CancelFunc - matches *sync.Map + matches *MapOf[uuid.UUID, *MatchHandler] matchCount *atomic.Int64 indexWriter *bluge.Writer @@ -168,7 +168,7 @@ func NewLocalMatchRegistry(logger, startupLogger *zap.Logger, config Config, ses ctx: ctx, ctxCancelFn: ctxCancelFn, - matches: &sync.Map{}, + matches: &MapOf[uuid.UUID, *MatchHandler]{}, matchCount: atomic.NewInt64(0), indexWriter: indexWriter, @@ -211,7 +211,7 @@ func (r *LocalMatchRegistry) processLabelUpdates(batch *index.Batch) { batch.Delete(bluge.Identifier(id)) continue } - doc, err := MapMatchIndexEntry(id, op, r.logger) + doc, err := MapMatchIndexEntry(id, op) if err != nil { r.logger.Error("error mapping match index entry to doc: %v", zap.Error(err)) } @@ -305,15 +305,14 @@ func (r *LocalMatchRegistry) GetMatch(ctx context.Context, id string) (*api.Matc if !ok { return nil, "", nil } - handler := mh.(*MatchHandler) return &api.Match{ - MatchId: handler.IDStr, + MatchId: mh.IDStr, Authoritative: true, - Label: &wrapperspb.StringValue{Value: handler.Label()}, - Size: int32(handler.PresenceList.Size()), - TickRate: int32(handler.Rate), - HandlerName: handler.Core.HandlerName(), + Label: &wrapperspb.StringValue{Value: mh.Label()}, + Size: int32(mh.PresenceList.Size()), + TickRate: int32(mh.Rate), + HandlerName: mh.Core.HandlerName(), }, r.node, nil } @@ -538,7 +537,7 @@ func (r *LocalMatchRegistry) ListMatches(ctx context.Context, limit int, authori if !ok { continue } - size := int32(mh.(*MatchHandler).PresenceList.Size()) + size := int32(mh.PresenceList.Size()) if minSize != nil && minSize.Value > size { // Not eligible based on minimum size. @@ -655,8 +654,8 @@ func (r *LocalMatchRegistry) Stop(graceSeconds int) chan struct{} { // If grace period is 0 stop match label processing immediately. r.ctxCancelFn() - r.matches.Range(func(id, mh interface{}) bool { - mh.(*MatchHandler).Stop() + r.matches.Range(func(id uuid.UUID, mh *MatchHandler) bool { + mh.Stop() return true }) // Termination was triggered and there are no active matches. @@ -669,10 +668,10 @@ func (r *LocalMatchRegistry) Stop(graceSeconds int) chan struct{} { } var anyRunning bool - r.matches.Range(func(id, mh interface{}) bool { + r.matches.Range(func(id uuid.UUID, mh *MatchHandler) bool { anyRunning = true // Don't care if the call queue is full, match is supposed to end anyway. - mh.(*MatchHandler).QueueTerminate(graceSeconds) + mh.QueueTerminate(graceSeconds) return true }) @@ -699,11 +698,10 @@ func (r *LocalMatchRegistry) JoinAttempt(ctx context.Context, id uuid.UUID, node return false, false, false, "", "", nil } - m, ok := r.matches.Load(id) + mh, ok := r.matches.Load(id) if !ok { return false, false, false, "", "", nil } - mh := m.(*MatchHandler) if mh.PresenceList.Contains(&PresenceID{Node: fromNode, SessionID: sessionID}) { // The user is already part of this match. @@ -737,7 +735,7 @@ func (r *LocalMatchRegistry) Join(id uuid.UUID, presences []*MatchPresence) { } // Doesn't matter if the call queue was full here. If the match is being closed then joins don't matter anyway. - mh.(*MatchHandler).QueueJoin(presences, true) + mh.QueueJoin(presences, true) } func (r *LocalMatchRegistry) Leave(id uuid.UUID, presences []*MatchPresence) { @@ -747,7 +745,7 @@ func (r *LocalMatchRegistry) Leave(id uuid.UUID, presences []*MatchPresence) { } // Doesn't matter if the call queue was full here. If the match is being closed then leaves don't matter anyway. - mh.(*MatchHandler).QueueLeave(presences) + mh.QueueLeave(presences) } func (r *LocalMatchRegistry) Kick(stream PresenceStream, presences []*MatchPresence) { @@ -769,7 +767,7 @@ func (r *LocalMatchRegistry) SendData(id uuid.UUID, node string, userID, session return } - mh.(*MatchHandler).QueueData(&MatchDataMessage{ + mh.QueueData(&MatchDataMessage{ UserID: userID, SessionID: sessionID, Username: username, @@ -802,11 +800,10 @@ func (r *LocalMatchRegistry) Signal(ctx context.Context, id, data string) (strin return "", runtime.ErrMatchNotFound } - m, ok := r.matches.Load(matchID) + mh, ok := r.matches.Load(matchID) if !ok { return "", runtime.ErrMatchNotFound } - mh := m.(*MatchHandler) resultCh := make(chan *MatchSignalResult, 1) if !mh.QueueSignal(ctx, resultCh, data) { @@ -841,11 +838,10 @@ func (r *LocalMatchRegistry) GetState(ctx context.Context, id uuid.UUID, node st return nil, 0, "", nil } - m, ok := r.matches.Load(id) + mh, ok := r.matches.Load(id) if !ok { return nil, 0, "", runtime.ErrMatchNotFound } - mh := m.(*MatchHandler) resultCh := make(chan *MatchGetStateResult, 1) if !mh.QueueGetState(ctx, resultCh) { @@ -880,7 +876,7 @@ func (r *LocalMatchRegistry) GetState(ctx context.Context, id uuid.UUID, node st } } -func MapMatchIndexEntry(id string, in *MatchIndexEntry, logger *zap.Logger) (*bluge.Document, error) { +func MapMatchIndexEntry(id string, in *MatchIndexEntry) (*bluge.Document, error) { rv := bluge.NewDocument(id) rv.AddField(bluge.NewKeywordField("node", in.Node).StoreValue()) diff --git a/server/party_registry.go b/server/party_registry.go index 63851b1aa..9e4ee3e3b 100644 --- a/server/party_registry.go +++ b/server/party_registry.go @@ -17,8 +17,6 @@ package server import ( "context" "errors" - "sync" - "github.com/gofrs/uuid" "github.com/heroiclabs/nakama-common/rtapi" "go.uber.org/zap" @@ -52,7 +50,7 @@ type LocalPartyRegistry struct { router MessageRouter node string - parties *sync.Map + parties *MapOf[uuid.UUID, *PartyHandler] } func NewLocalPartyRegistry(logger *zap.Logger, matchmaker Matchmaker, tracker Tracker, streamManager StreamManager, router MessageRouter, node string) PartyRegistry { @@ -64,7 +62,7 @@ func NewLocalPartyRegistry(logger *zap.Logger, matchmaker Matchmaker, tracker Tr router: router, node: node, - parties: &sync.Map{}, + parties: &MapOf[uuid.UUID, *PartyHandler]{}, } } @@ -86,7 +84,7 @@ func (p *LocalPartyRegistry) Join(id uuid.UUID, presences []*Presence) { if !found { return } - ph.(*PartyHandler).Join(presences) + ph.Join(presences) } func (p *LocalPartyRegistry) Leave(id uuid.UUID, presences []*Presence) { @@ -94,7 +92,7 @@ func (p *LocalPartyRegistry) Leave(id uuid.UUID, presences []*Presence) { if !found { return } - ph.(*PartyHandler).Leave(presences) + ph.Leave(presences) } func (p *LocalPartyRegistry) PartyJoinRequest(ctx context.Context, id uuid.UUID, node string, presence *Presence) (bool, error) { @@ -107,7 +105,7 @@ func (p *LocalPartyRegistry) PartyJoinRequest(ctx context.Context, id uuid.UUID, return false, ErrPartyNotFound } - return ph.(*PartyHandler).JoinRequest(presence) + return ph.JoinRequest(presence) } func (p *LocalPartyRegistry) PartyPromote(ctx context.Context, id uuid.UUID, node, sessionID, fromNode string, presence *rtapi.UserPresence) error { @@ -120,7 +118,7 @@ func (p *LocalPartyRegistry) PartyPromote(ctx context.Context, id uuid.UUID, nod return ErrPartyNotFound } - return ph.(*PartyHandler).Promote(sessionID, fromNode, presence) + return ph.Promote(sessionID, fromNode, presence) } func (p *LocalPartyRegistry) PartyAccept(ctx context.Context, id uuid.UUID, node, sessionID, fromNode string, presence *rtapi.UserPresence) error { @@ -133,7 +131,7 @@ func (p *LocalPartyRegistry) PartyAccept(ctx context.Context, id uuid.UUID, node return ErrPartyNotFound } - return ph.(*PartyHandler).Accept(sessionID, fromNode, presence) + return ph.Accept(sessionID, fromNode, presence) } func (p *LocalPartyRegistry) PartyRemove(ctx context.Context, id uuid.UUID, node, sessionID, fromNode string, presence *rtapi.UserPresence) error { @@ -146,7 +144,7 @@ func (p *LocalPartyRegistry) PartyRemove(ctx context.Context, id uuid.UUID, node return ErrPartyNotFound } - return ph.(*PartyHandler).Remove(sessionID, fromNode, presence) + return ph.Remove(sessionID, fromNode, presence) } func (p *LocalPartyRegistry) PartyClose(ctx context.Context, id uuid.UUID, node, sessionID, fromNode string) error { @@ -159,7 +157,7 @@ func (p *LocalPartyRegistry) PartyClose(ctx context.Context, id uuid.UUID, node, return ErrPartyNotFound } - return ph.(*PartyHandler).Close(sessionID, fromNode) + return ph.Close(sessionID, fromNode) } func (p *LocalPartyRegistry) PartyJoinRequestList(ctx context.Context, id uuid.UUID, node, sessionID, fromNode string) ([]*rtapi.UserPresence, error) { @@ -172,7 +170,7 @@ func (p *LocalPartyRegistry) PartyJoinRequestList(ctx context.Context, id uuid.U return nil, ErrPartyNotFound } - return ph.(*PartyHandler).JoinRequestList(sessionID, fromNode) + return ph.JoinRequestList(sessionID, fromNode) } func (p *LocalPartyRegistry) PartyMatchmakerAdd(ctx context.Context, id uuid.UUID, node, sessionID, fromNode, query string, minCount, maxCount, countMultiple int, stringProperties map[string]string, numericProperties map[string]float64) (string, []*PresenceID, error) { @@ -185,7 +183,7 @@ func (p *LocalPartyRegistry) PartyMatchmakerAdd(ctx context.Context, id uuid.UUI return "", nil, ErrPartyNotFound } - return ph.(*PartyHandler).MatchmakerAdd(sessionID, fromNode, query, minCount, maxCount, countMultiple, stringProperties, numericProperties) + return ph.MatchmakerAdd(sessionID, fromNode, query, minCount, maxCount, countMultiple, stringProperties, numericProperties) } func (p *LocalPartyRegistry) PartyMatchmakerRemove(ctx context.Context, id uuid.UUID, node, sessionID, fromNode, ticket string) error { @@ -198,7 +196,7 @@ func (p *LocalPartyRegistry) PartyMatchmakerRemove(ctx context.Context, id uuid. return ErrPartyNotFound } - return ph.(*PartyHandler).MatchmakerRemove(sessionID, fromNode, ticket) + return ph.MatchmakerRemove(sessionID, fromNode, ticket) } func (p *LocalPartyRegistry) PartyDataSend(ctx context.Context, id uuid.UUID, node, sessionID, fromNode string, opCode int64, data []byte) error { @@ -211,5 +209,5 @@ func (p *LocalPartyRegistry) PartyDataSend(ctx context.Context, id uuid.UUID, no return ErrPartyNotFound } - return ph.(*PartyHandler).DataSend(sessionID, fromNode, opCode, data) + return ph.DataSend(sessionID, fromNode, opCode, data) } diff --git a/server/runtime_lua.go b/server/runtime_lua.go index da7be50ce..9d4e1645f 100644 --- a/server/runtime_lua.go +++ b/server/runtime_lua.go @@ -53,9 +53,9 @@ func (s *LSentinelType) Type() lua.LValueType { return LTSentinel } var LSentinel = lua.LValue(&LSentinelType{}) type RuntimeLuaCallbacks struct { - RPC *sync.Map - Before *sync.Map - After *sync.Map + RPC *MapOf[string, *lua.LFunction] + Before *MapOf[string, *lua.LFunction] + After *MapOf[string, *lua.LFunction] Matchmaker *lua.LFunction TournamentEnd *lua.LFunction TournamentReset *lua.LFunction @@ -1964,19 +1964,19 @@ func (r *RuntimeLua) GetCallback(e RuntimeExecutionMode, key string) *lua.LFunct if !found { return nil } - return fn.(*lua.LFunction) + return fn case RuntimeExecutionModeBefore: fn, found := r.callbacks.Before.Load(key) if !found { return nil } - return fn.(*lua.LFunction) + return fn case RuntimeExecutionModeAfter: fn, found := r.callbacks.After.Load(key) if !found { return nil } - return fn.(*lua.LFunction) + return fn case RuntimeExecutionModeMatchmaker: return r.callbacks.Matchmaker case RuntimeExecutionModeTournamentEnd: @@ -2142,9 +2142,9 @@ func newRuntimeLuaVM(logger *zap.Logger, db *sql.DB, protojsonMarshaler *protojs vm.Call(1, 0) } callbacks := &RuntimeLuaCallbacks{ - RPC: &sync.Map{}, - Before: &sync.Map{}, - After: &sync.Map{}, + RPC: &MapOf[string, *lua.LFunction]{}, + Before: &MapOf[string, *lua.LFunction]{}, + After: &MapOf[string, *lua.LFunction]{}, } registerCallbackFn := func(e RuntimeExecutionMode, key string, fn *lua.LFunction) { switch e { diff --git a/server/session_registry.go b/server/session_registry.go index a1cb5bc83..49e9ad622 100644 --- a/server/session_registry.go +++ b/server/session_registry.go @@ -18,7 +18,6 @@ import ( "context" "github.com/heroiclabs/nakama-common/api" "google.golang.org/protobuf/types/known/timestamppb" - "sync" "time" "github.com/gofrs/uuid" @@ -72,7 +71,7 @@ type SessionRegistry interface { type LocalSessionRegistry struct { metrics Metrics - sessions *sync.Map + sessions *MapOf[uuid.UUID, Session] sessionCount *atomic.Int32 } @@ -80,7 +79,7 @@ func NewLocalSessionRegistry(metrics Metrics) SessionRegistry { return &LocalSessionRegistry{ metrics: metrics, - sessions: &sync.Map{}, + sessions: &MapOf[uuid.UUID, Session]{}, sessionCount: atomic.NewInt32(0), } } @@ -96,7 +95,7 @@ func (r *LocalSessionRegistry) Get(sessionID uuid.UUID) Session { if !ok { return nil } - return session.(Session) + return session } func (r *LocalSessionRegistry) Add(session Session) { @@ -119,7 +118,7 @@ func (r *LocalSessionRegistry) Disconnect(ctx context.Context, sessionID uuid.UU if len(reason) > 0 { reasonOverride = reason[0] } - session.(Session).Close("server-side session disconnect", reasonOverride) + session.Close("server-side session disconnect", reasonOverride) } return nil } @@ -134,7 +133,7 @@ func (r *LocalSessionRegistry) SingleSession(ctx context.Context, tracker Tracke session, ok := r.sessions.Load(foundSessionID) if ok { // No need to remove the session from the map, session.Close() will do that. - session.(Session).Close("server-side session disconnect", runtime.PresenceReasonDisconnect, + session.Close("server-side session disconnect", runtime.PresenceReasonDisconnect, &rtapi.Envelope{Message: &rtapi.Envelope_Notifications{ Notifications: &rtapi.Notifications{ Notifications: []*api.Notification{ diff --git a/vendor/github.com/dlclark/regexp2/syntax/parser.go b/vendor/github.com/dlclark/regexp2/syntax/parser.go index efa55986c..9dc6e3130 100644 --- a/vendor/github.com/dlclark/regexp2/syntax/parser.go +++ b/vendor/github.com/dlclark/regexp2/syntax/parser.go @@ -1427,7 +1427,7 @@ func (p *parser) scanCapname() string { return string(p.pattern[startpos:p.textpos()]) } -// Scans contents of [] (not including []'s), and converts to a set. +//Scans contents of [] (not including []'s), and converts to a set. func (p *parser) scanCharSet(caseInsensitive, scanOnly bool) (*CharSet, error) { ch := '\x00' chPrev := '\x00' diff --git a/vendor/github.com/dop251/goja/ast/node.go b/vendor/github.com/dop251/goja/ast/node.go index ba3305a52..0ee80ec9a 100644 --- a/vendor/github.com/dop251/goja/ast/node.go +++ b/vendor/github.com/dop251/goja/ast/node.go @@ -1,10 +1,11 @@ /* Package ast declares types representing a JavaScript AST. -# Warning +Warning The parser and AST interfaces are still works-in-progress (particularly where node types are concerned) and may change in the future. + */ package ast diff --git a/vendor/github.com/dop251/goja/parser/error.go b/vendor/github.com/dop251/goja/parser/error.go index cf4d2c381..4c109d6c9 100644 --- a/vendor/github.com/dop251/goja/parser/error.go +++ b/vendor/github.com/dop251/goja/parser/error.go @@ -122,6 +122,7 @@ func (self *_parser) errorUnexpectedToken(tkn token.Token) error { } // ErrorList is a list of *Errors. +// type ErrorList []*Error // Add adds an Error with given position and message to an ErrorList. diff --git a/vendor/github.com/dop251/goja/runtime.go b/vendor/github.com/dop251/goja/runtime.go index 02d2c5ff5..35d839dff 100644 --- a/vendor/github.com/dop251/goja/runtime.go +++ b/vendor/github.com/dop251/goja/runtime.go @@ -1300,10 +1300,10 @@ func MustCompile(name, src string, strict bool) *Program { // Parse takes a source string and produces a parsed AST. Use this function if you want to pass options // to the parser, e.g.: // -// p, err := Parse("test.js", "var a = true", parser.WithDisableSourceMaps) -// if err != nil { /* ... */ } -// prg, err := CompileAST(p, true) -// // ... +// p, err := Parse("test.js", "var a = true", parser.WithDisableSourceMaps) +// if err != nil { /* ... */ } +// prg, err := CompileAST(p, true) +// // ... // // Otherwise use Compile which combines both steps. func Parse(name, src string, options ...parser.Option) (prg *js_ast.Program, err error) { @@ -1475,32 +1475,32 @@ them in ECMAScript, bear in mind the following caveats: 1. If a regular JavaScript Object is assigned as an element of a wrapped Go struct, map or array, it is Export()'ed and therefore copied. This may result in an unexpected behaviour in JavaScript: - m := map[string]interface{}{} - vm.Set("m", m) - vm.RunString(` - var obj = {test: false}; - m.obj = obj; // obj gets Export()'ed, i.e. copied to a new map[string]interface{} and then this map is set as m["obj"] - obj.test = true; // note, m.obj.test is still false - `) - fmt.Println(m["obj"].(map[string]interface{})["test"]) // prints "false" + m := map[string]interface{}{} + vm.Set("m", m) + vm.RunString(` + var obj = {test: false}; + m.obj = obj; // obj gets Export()'ed, i.e. copied to a new map[string]interface{} and then this map is set as m["obj"] + obj.test = true; // note, m.obj.test is still false + `) + fmt.Println(m["obj"].(map[string]interface{})["test"]) // prints "false" 2. Be careful with nested non-pointer compound types (structs, slices and arrays) if you modify them in ECMAScript. Better avoid it at all if possible. One of the fundamental differences between ECMAScript and Go is in the former all Objects are references whereas in Go you can have a literal struct or array. Consider the following example: - type S struct { - Field int - } + type S struct { + Field int + } - a := []S{{1}, {2}} // slice of literal structs - vm.Set("a", &a) - vm.RunString(` - let tmp = {Field: 1}; - a[0] = tmp; - a[1] = tmp; - tmp.Field = 2; - `) + a := []S{{1}, {2}} // slice of literal structs + vm.Set("a", &a) + vm.RunString(` + let tmp = {Field: 1}; + a[0] = tmp; + a[1] = tmp; + tmp.Field = 2; + `) In ECMAScript one would expect a[0].Field and a[1].Field to be equal to 2, but this is really not possible (or at least non-trivial without some complex reference tracking). @@ -1516,29 +1516,29 @@ in copying of a[0]. (e.g. by direct assignment, deletion or shrinking the array) the old a[0] is copied and the earlier returned value becomes a reference to the copy: - let tmp = a[0]; // no copy, tmp is a reference to a[0] - tmp.Field = 1; // a[0].Field === 1 after this - a[0] = {Field: 2}; // tmp is now a reference to a copy of the old value (with Field === 1) - a[0].Field === 2 && tmp.Field === 1; // true + let tmp = a[0]; // no copy, tmp is a reference to a[0] + tmp.Field = 1; // a[0].Field === 1 after this + a[0] = {Field: 2}; // tmp is now a reference to a copy of the old value (with Field === 1) + a[0].Field === 2 && tmp.Field === 1; // true * Array value swaps caused by in-place sort (using Array.prototype.sort()) do not count as re-assignments, instead the references are adjusted to point to the new indices. * Assignment to an inner compound value always does a copy (and sometimes type conversion): - a[1] = tmp; // a[1] is now a copy of tmp - tmp.Field = 3; // does not affect a[1].Field + a[1] = tmp; // a[1] is now a copy of tmp + tmp.Field = 3; // does not affect a[1].Field 3. Non-addressable structs, slices and arrays get copied. This sometimes may lead to a confusion as assigning to inner fields does not appear to work: - a1 := []interface{}{S{1}, S{2}} - vm.Set("a1", &a1) - vm.RunString(` - a1[0].Field === 1; // true - a1[0].Field = 2; - a1[0].Field === 2; // FALSE, because what it really did was copy a1[0] set its Field to 2 and immediately drop it - `) + a1 := []interface{}{S{1}, S{2}} + vm.Set("a1", &a1) + vm.RunString(` + a1[0].Field === 1; // true + a1[0].Field = 2; + a1[0].Field === 2; // FALSE, because what it really did was copy a1[0] set its Field to 2 and immediately drop it + `) An alternative would be making a1[0].Field a non-writable property which would probably be more in line with ECMAScript, however it would require to manually copy the value if it does need to be modified which may be @@ -1548,29 +1548,29 @@ Note, the same applies to slices. If a slice is passed by value (not as a pointe value. Moreover, extending the slice may result in the underlying array being re-allocated and copied. For example: - a := []interface{}{1} - vm.Set("a", a) - vm.RunString(`a.push(2); a[0] = 0;`) - fmt.Println(a[0]) // prints "1" + a := []interface{}{1} + vm.Set("a", a) + vm.RunString(`a.push(2); a[0] = 0;`) + fmt.Println(a[0]) // prints "1" Notes on individual types: -# Primitive types +Primitive types Primitive types (numbers, string, bool) are converted to the corresponding JavaScript primitives. -# Strings +Strings Because of the difference in internal string representation between ECMAScript (which uses UTF-16) and Go (which uses UTF-8) conversion from JS to Go may be lossy. In particular, code points that can be part of UTF-16 surrogate pairs (0xD800-0xDFFF) cannot be represented in UTF-8 unless they form a valid surrogate pair and are replaced with utf8.RuneError. -# Nil +Nil Nil is converted to null. -# Functions +Functions func(FunctionCall) Value is treated as a native JavaScript function. This increases performance because there are no automatic argument and return value type conversions (which involves reflect). Attempting to use @@ -1581,33 +1581,33 @@ func(FunctionCall, *Runtime) Value is treated as above, except the *Runtime is a func(ConstructorCall) *Object is treated as a native constructor, allowing to use it with the new operator: - func MyObject(call goja.ConstructorCall) *goja.Object { - // call.This contains the newly created object as per http://www.ecma-international.org/ecma-262/5.1/index.html#sec-13.2.2 - // call.Arguments contain arguments passed to the function + func MyObject(call goja.ConstructorCall) *goja.Object { + // call.This contains the newly created object as per http://www.ecma-international.org/ecma-262/5.1/index.html#sec-13.2.2 + // call.Arguments contain arguments passed to the function - call.This.Set("method", method) + call.This.Set("method", method) - //... + //... - // If return value is a non-nil *Object, it will be used instead of call.This - // This way it is possible to return a Go struct or a map converted - // into goja.Value using ToValue(), however in this case - // instanceof will not work as expected, unless you set the prototype: - // - // instance := &myCustomStruct{} - // instanceValue := vm.ToValue(instance).(*Object) - // instanceValue.SetPrototype(call.This.Prototype()) - // return instanceValue - return nil - } + // If return value is a non-nil *Object, it will be used instead of call.This + // This way it is possible to return a Go struct or a map converted + // into goja.Value using ToValue(), however in this case + // instanceof will not work as expected, unless you set the prototype: + // + // instance := &myCustomStruct{} + // instanceValue := vm.ToValue(instance).(*Object) + // instanceValue.SetPrototype(call.This.Prototype()) + // return instanceValue + return nil + } - runtime.Set("MyObject", MyObject) + runtime.Set("MyObject", MyObject) Then it can be used in JS as follows: - var o = new MyObject(arg); - var o1 = MyObject(arg); // same thing - o instanceof MyObject && o1 instanceof MyObject; // true + var o = new MyObject(arg); + var o1 = MyObject(arg); // same thing + o instanceof MyObject && o1 instanceof MyObject; // true When a native constructor is called directly (without the new operator) its behavior depends on this value: if it's an Object, it is passed through, otherwise a new one is created exactly as @@ -1624,7 +1624,7 @@ converted into a JS exception. If the error is *Exception, it is thrown as is, o Note that if there are exactly two return values and the last is an `error`, the function returns the first value as is, not an Array. -# Structs +Structs Structs are converted to Object-like values. Fields and methods are available as properties, their values are results of this method (ToValue()) applied to the corresponding Go value. @@ -1635,29 +1635,29 @@ Attempt to define a new property or delete an existing property will fail (throw property. Symbol properties only exist in the wrapper and do not affect the underlying Go value. Note that because a wrapper is created every time a property is accessed it may lead to unexpected results such as this: - type Field struct{ - } - type S struct { - Field *Field - } - var s = S{ - Field: &Field{}, - } - vm := New() - vm.Set("s", &s) - res, err := vm.RunString(` - var sym = Symbol(66); - var field1 = s.Field; - field1[sym] = true; - var field2 = s.Field; - field1 === field2; // true, because the equality operation compares the wrapped values, not the wrappers - field1[sym] === true; // true - field2[sym] === undefined; // also true - `) + type Field struct{ + } + type S struct { + Field *Field + } + var s = S{ + Field: &Field{}, + } + vm := New() + vm.Set("s", &s) + res, err := vm.RunString(` + var sym = Symbol(66); + var field1 = s.Field; + field1[sym] = true; + var field2 = s.Field; + field1 === field2; // true, because the equality operation compares the wrapped values, not the wrappers + field1[sym] === true; // true + field2[sym] === undefined; // also true + `) The same applies to values from maps and slices as well. -# Handling of time.Time +Handling of time.Time time.Time does not get special treatment and therefore is converted just like any other `struct` providing access to all its methods. This is done deliberately instead of converting it to a `Date` because these two types are not fully @@ -1666,32 +1666,32 @@ result in a loss of information. If you need to convert it to a `Date`, it can be done either in JS: - var d = new Date(goval.UnixNano()/1e6); + var d = new Date(goval.UnixNano()/1e6); ... or in Go: - now := time.Now() - vm := New() - val, err := vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(now.UnixNano()/1e6)) - if err != nil { - ... - } - vm.Set("d", val) + now := time.Now() + vm := New() + val, err := vm.New(vm.Get("Date").ToObject(vm), vm.ToValue(now.UnixNano()/1e6)) + if err != nil { + ... + } + vm.Set("d", val) Note that Value.Export() for a `Date` value returns time.Time in local timezone. -# Maps +Maps Maps with string or integer key type are converted into host objects that largely behave like a JavaScript Object. -# Maps with methods +Maps with methods If a map type has at least one method defined, the properties of the resulting Object represent methods, not map keys. This is because in JavaScript there is no distinction between 'object.key` and `object[key]`, unlike Go. If access to the map values is required, it can be achieved by defining another method or, if it's not possible, by defining an external getter function. -# Slices +Slices Slices are converted into host objects that behave largely like JavaScript Array. It has the appropriate prototype and all the usual methods should work. There is, however, a caveat: converted Arrays may not contain holes @@ -1700,7 +1700,7 @@ an index < length will set it to a zero value (but the property will remain). Ni `null`. Accessing an element beyond `length` returns `undefined`. Also see the warning above about passing slices as values (as opposed to pointers). -# Arrays +Arrays Arrays are converted similarly to slices, except the resulting Arrays are not resizable (and therefore the 'length' property is non-writable). @@ -2199,16 +2199,16 @@ func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect. // // Notes on specific cases: // -// # Empty interface +// Empty interface // // Exporting to an interface{} results in a value of the same type as Value.Export() would produce. // -// # Numeric types +// Numeric types // // Exporting to numeric types uses the standard ECMAScript conversion operations, same as used when assigning // values to non-clamped typed array items, e.g. https://262.ecma-international.org/#sec-toint32. // -// # Functions +// Functions // // Exporting to a 'func' creates a strictly typed 'gateway' into an ES function which can be called from Go. // The arguments are converted into ES values using Runtime.ToValue(). If the func has no return values, @@ -2225,7 +2225,7 @@ func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect. // // For a more low-level mechanism see AssertFunction(). // -// # Map types +// Map types // // An ES Map can be exported into a Go map type. If any exported key value is non-hashable, the operation panics // (as reflect.Value.SetMapIndex() would). Symbol.iterator is ignored. @@ -2236,7 +2236,7 @@ func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect. // // Any other Object populates the map with own enumerable non-symbol properties. // -// # Slice types +// Slice types // // Exporting an ES Set into a slice type results in its elements being exported. // @@ -2250,17 +2250,18 @@ func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect. // // For any other Object an error is returned. // -// # Array types +// Array types // // Anything that can be exported to a slice type can also be exported to an array type, as long as the lengths // match. If they do not, an error is returned. // -// # Proxy +// Proxy // // Proxy objects are treated the same way as if they were accessed from ES code in regard to their properties // (such as 'length' or [Symbol.iterator]). This means exporting them to slice types works, however // exporting a proxied Map into a map type does not produce its contents, because the Proxy is not recognised // as a Map. Same applies to a proxied Set. +// func (r *Runtime) ExportTo(v Value, target interface{}) error { tval := reflect.ValueOf(target) if tval.Kind() != reflect.Ptr || tval.IsNil() { diff --git a/vendor/github.com/dop251/goja/token/token.go b/vendor/github.com/dop251/goja/token/token.go index 8527137b6..2ae405a76 100644 --- a/vendor/github.com/dop251/goja/token/token.go +++ b/vendor/github.com/dop251/goja/token/token.go @@ -13,6 +13,7 @@ type Token int // token string (e.g., for the token PLUS, the String() is // "+"). For all other tokens the string corresponds to the token // name (e.g. for the token IDENTIFIER, the string is "IDENTIFIER"). +// func (tkn Token) String() string { if tkn == 0 { return "UNKNOWN" @@ -85,24 +86,25 @@ type _keyword struct { // // 7.6.1.2 Future Reserved Words: // -// const -// class -// enum -// export -// extends -// import -// super +// const +// class +// enum +// export +// extends +// import +// super // // 7.6.1.2 Future Reserved Words (strict): // -// implements -// interface -// let -// package -// private -// protected -// public -// static +// implements +// interface +// let +// package +// private +// protected +// public +// static +// func IsKeyword(literal string) (Token, bool) { if keyword, exists := keywordTable[literal]; exists { if keyword.futureKeyword { -- GitLab