mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			211 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			211 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /*
 | |
|   # Implementation strategy
 | |
| 
 | |
|   Create a tree of `Map`s, such that indexing the tree recursively (with items
 | |
|   of a key array, sequentially), traverses the tree, so that when the key array
 | |
|   is exhausted, the tree node we arrive at contains the value for that key
 | |
|   array under the guaranteed-unique `Symbol` key `dataSymbol`.
 | |
| 
 | |
|   ## Example
 | |
| 
 | |
|   Start with an empty `ArrayKeyedMap` tree:
 | |
| 
 | |
|       {
 | |
|       }
 | |
| 
 | |
|   Add ['a'] → 1:
 | |
| 
 | |
|       {
 | |
|         'a': {
 | |
|           [dataSymbol]: 1,
 | |
|         },
 | |
|       }
 | |
| 
 | |
|   Add [] → 0:
 | |
| 
 | |
|       {
 | |
|         [dataSymbol]: 0,
 | |
|         'a': {
 | |
|           [dataSymbol]: 1,
 | |
|         },
 | |
|       }
 | |
| 
 | |
|   Add ['a', 'b', 'c', 'd'] → 4:
 | |
| 
 | |
|       {
 | |
|         [dataSymbol]: 0,
 | |
|         'a': {
 | |
|           [dataSymbol]: 1,
 | |
|           'b': {
 | |
|             'c': {
 | |
|               'd': {
 | |
|                 [dataSymbol]: 4,
 | |
|               },
 | |
|             },
 | |
|           },
 | |
|         },
 | |
|       }
 | |
| 
 | |
|   String array keys are used in the above example for simplicity.  In reality,
 | |
|   we can support any values in array keys, because `Map`s do.
 | |
| */
 | |
| 
 | |
| const dataSymbol = Symbol('path-store-trunk')
 | |
| 
 | |
| //
 | |
| // This class represents the external API
 | |
| //
 | |
| 
 | |
| class ArrayKeyedMap {
 | |
|   constructor (initialEntries = []) {
 | |
|     this._root = new Map()
 | |
|     this._size = 0
 | |
|     for (const [k, v] of initialEntries) { this.set(k, v) }
 | |
|   }
 | |
| 
 | |
|   set (path, value) { return set.call(this, path, value) }
 | |
| 
 | |
|   has (path) { return has.call(this, path) }
 | |
| 
 | |
|   get (path) { return get.call(this, path) }
 | |
| 
 | |
|   delete (path) { return del.call(this, path) }
 | |
| 
 | |
|   get size () { return this._size }
 | |
| 
 | |
|   clear () {
 | |
|     this._root.clear()
 | |
|     this._size = 0
 | |
|   }
 | |
| 
 | |
|   hasPrefix (path) { return hasPrefix.call(this, path) }
 | |
| 
 | |
|   get [Symbol.toStringTag] () { return 'ArrayKeyedMap' }
 | |
| 
 | |
|   * [Symbol.iterator] () { yield * entries.call(this) }
 | |
| 
 | |
|   * entries () { yield * entries.call(this) }
 | |
| 
 | |
|   * keys () { yield * keys.call(this) }
 | |
| 
 | |
|   * values () { yield * values.call(this) }
 | |
| 
 | |
|   forEach (callback, thisArg) { forEach.call(this, callback, thisArg) }
 | |
| }
 | |
| 
 | |
| //
 | |
| // These stateless functions implement the internals
 | |
| //
 | |
| 
 | |
| function set (path, value) {
 | |
|   let map = this._root
 | |
|   for (const item of path) {
 | |
|     let nextMap = map.get(item)
 | |
|     if (!nextMap) {
 | |
|       // Create next map if none exists
 | |
|       nextMap = new Map()
 | |
|       map.set(item, nextMap)
 | |
|     }
 | |
|     map = nextMap
 | |
|   }
 | |
| 
 | |
|   // Reached end of path.  Set the data symbol to the given value, and
 | |
|   // increment size if nothing was here before.
 | |
|   if (!map.has(dataSymbol)) this._size += 1
 | |
|   map.set(dataSymbol, value)
 | |
|   return this
 | |
| }
 | |
| 
 | |
| function has (path) {
 | |
|   let map = this._root
 | |
|   for (const item of path) {
 | |
|     const nextMap = map.get(item)
 | |
|     if (nextMap) {
 | |
|       map = nextMap
 | |
|     } else {
 | |
|       return false
 | |
|     }
 | |
|   }
 | |
|   return map.has(dataSymbol)
 | |
| }
 | |
| 
 | |
| function get (path) {
 | |
|   let map = this._root
 | |
|   for (const item of path) {
 | |
|     map = map.get(item)
 | |
|     if (!map) return undefined
 | |
|   }
 | |
|   return map.get(dataSymbol)
 | |
| }
 | |
| 
 | |
| function del (path) {
 | |
|   let map = this._root
 | |
| 
 | |
|   // Maintain a stack of maps we visited, so we can go back and trim empty ones
 | |
|   // if we delete something.
 | |
|   const stack = []
 | |
| 
 | |
|   for (const item of path) {
 | |
|     const nextMap = map.get(item)
 | |
|     if (nextMap) {
 | |
|       stack.unshift({ parent: map, child: nextMap, item })
 | |
|       map = nextMap
 | |
|     } else {
 | |
|       // Nothing to delete
 | |
|       return false
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // Reached end of path.  Delete data, if it exists.
 | |
|   const hadPreviousValue = map.delete(dataSymbol)
 | |
| 
 | |
|   // If something was deleted, decrement size and go through the stack of
 | |
|   // visited maps, trimming any that are now empty.
 | |
|   if (hadPreviousValue) {
 | |
|     this._size -= 1
 | |
| 
 | |
|     for (const { parent, child, item } of stack) {
 | |
|       if (child.size === 0) {
 | |
|         parent.delete(item)
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   return hadPreviousValue
 | |
| }
 | |
| 
 | |
| function hasPrefix (path) {
 | |
|   let map = this._root
 | |
|   for (const item of path) {
 | |
|     map = map.get(item)
 | |
|     if (!map) return false
 | |
|   }
 | |
|   return true
 | |
| }
 | |
| 
 | |
| function * entries () {
 | |
|   const stack = [{ path: [], map: this._root }]
 | |
|   while (stack.length > 0) {
 | |
|     const { path, map } = stack.pop()
 | |
|     for (const [k, v] of map.entries()) {
 | |
|       if (k === dataSymbol) yield [path, v]
 | |
|       else stack.push({ path: path.concat([k]), map: v })
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| function * keys () {
 | |
|   for (const [k] of this.entries()) yield k
 | |
| }
 | |
| 
 | |
| function * values () {
 | |
|   for (const [, v] of this.entries()) yield v
 | |
| }
 | |
| 
 | |
| function forEach (callback, thisArg) {
 | |
|   for (const [k, v] of this.entries()) callback.call(thisArg, v, k, this)
 | |
| }
 | |
| 
 | |
| export {
 | |
|     ArrayKeyedMap
 | |
| }
 |