diff --git a/lib/collections/unordered-map.d.ts b/lib/collections/unordered-map.d.ts index dd96c875..60e02e95 100644 --- a/lib/collections/unordered-map.d.ts +++ b/lib/collections/unordered-map.d.ts @@ -1,13 +1,12 @@ import { Bytes } from "../utils"; import { Vector } from "./vector"; +import { LookupMap } from "./lookup-map"; export declare class UnorderedMap { readonly prefix: Bytes; - readonly keyIndexPrefix: Bytes; readonly keys: Vector; - readonly values: Vector; + readonly values: LookupMap; constructor(prefix: Bytes); get length(): number; - private set length(value); isEmpty(): boolean; get(key: Bytes): unknown | null; set(key: Bytes, value: unknown): unknown | null; @@ -21,7 +20,7 @@ export declare class UnorderedMap { } declare class UnorderedMapIterator { private keys; - private values; + private map; constructor(unorderedMap: UnorderedMap); next(): { value: [unknown | null, unknown | null]; diff --git a/lib/collections/unordered-map.js b/lib/collections/unordered-map.js index 9c434af7..e99436b3 100644 --- a/lib/collections/unordered-map.js +++ b/lib/collections/unordered-map.js @@ -1,116 +1,68 @@ -import * as near from "../api"; -import { u8ArrayToBytes, bytesToU8Array } from "../utils"; import { Vector, VectorIterator } from "./vector"; +import { LookupMap } from "./lookup-map"; const ERR_INCONSISTENT_STATE = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; -function serializeIndex(index) { - let data = new Uint32Array([index]); - let array = new Uint8Array(data.buffer); - return u8ArrayToBytes(array); -} -function deserializeIndex(rawIndex) { - let array = bytesToU8Array(rawIndex); - let data = new Uint32Array(array.buffer); - return data[0]; -} -function getIndexRaw(keyIndexPrefix, key) { - let indexLookup = keyIndexPrefix + JSON.stringify(key); - let indexRaw = near.storageRead(indexLookup); - return indexRaw; -} export class UnorderedMap { constructor(prefix) { this.prefix = prefix; - this.keyIndexPrefix = prefix + "i"; - let indexKey = prefix + "k"; - let indexValue = prefix + "v"; - this.keys = new Vector(indexKey); - this.values = new Vector(indexValue); + this.keys = new Vector(prefix + 'u'); // intentional different prefix with old UnorderedMap + this.values = new LookupMap(prefix + 'm'); } get length() { let keysLen = this.keys.length; - let valuesLen = this.values.length; - if (keysLen != valuesLen) { - throw new Error(ERR_INCONSISTENT_STATE); - } return keysLen; } - // noop, called by deserialize - set length(_l) { } isEmpty() { let keysIsEmpty = this.keys.isEmpty(); - let valuesIsEmpty = this.values.isEmpty(); - if (keysIsEmpty != valuesIsEmpty) { - throw new Error(ERR_INCONSISTENT_STATE); - } return keysIsEmpty; } get(key) { - let indexRaw = getIndexRaw(this.keyIndexPrefix, key); - if (indexRaw) { - let index = deserializeIndex(indexRaw); - let value = this.values.get(index); - if (value) { - return value; - } - else { - throw new Error(ERR_INCONSISTENT_STATE); - } + let valueAndIndex = this.values.get(key); + if (valueAndIndex === null) { + return null; } - return null; + let value = valueAndIndex[0]; + return value; } set(key, value) { - let indexLookup = this.keyIndexPrefix + JSON.stringify(key); - let indexRaw = near.storageRead(indexLookup); - if (indexRaw) { - let index = deserializeIndex(indexRaw); - return this.values.replace(index, value); - } - else { - let nextIndex = this.length; - let nextIndexRaw = serializeIndex(nextIndex); - near.storageWrite(indexLookup, nextIndexRaw); - this.keys.push(key); - this.values.push(value); - return null; + let valueAndIndex = this.values.get(key); + if (valueAndIndex !== null) { + let oldValue = valueAndIndex[0]; + valueAndIndex[0] = value; + this.values.set(key, valueAndIndex); + return oldValue; } + let nextIndex = this.length; + this.keys.push(key); + this.values.set(key, [value, nextIndex]); + return null; } remove(key) { - let indexLookup = this.keyIndexPrefix + JSON.stringify(key); - let indexRaw = near.storageRead(indexLookup); - if (indexRaw) { - if (this.length == 1) { - // If there is only one element then swap remove simply removes it without - // swapping with the last element. - near.storageRemove(indexLookup); - } - else { - // If there is more than one element then swap remove swaps it with the last - // element. - let lastKey = this.keys.get(this.length - 1); - if (!lastKey) { - throw new Error(ERR_INCONSISTENT_STATE); - } - near.storageRemove(indexLookup); - // If the removed element was the last element from keys, then we don't need to - // reinsert the lookup back. - if (lastKey != key) { - let lastLookupKey = this.keyIndexPrefix + JSON.stringify(lastKey); - near.storageWrite(lastLookupKey, indexRaw); - } + let oldValueAndIndex = this.values.remove(key); + if (oldValueAndIndex === null) { + return null; + } + let index = oldValueAndIndex[1]; + if (this.keys.swapRemove(index) === null) { + throw new Error(ERR_INCONSISTENT_STATE); + } + // the last key is swapped to key[index], the corresponding [value, index] need update + if (this.keys.length > 0 && index != this.keys.length) { + // if there is still elements and it was not the last element + let swappedKey = this.keys.get(index); + let swappedValueAndIndex = this.values.get(swappedKey); + if (swappedValueAndIndex === null) { + throw new Error(ERR_INCONSISTENT_STATE); } - let index = deserializeIndex(indexRaw); - this.keys.swapRemove(index); - return this.values.swapRemove(index); + this.values.set(swappedKey, [swappedValueAndIndex[0], index]); } - return null; + return oldValueAndIndex[0]; } clear() { for (let key of this.keys) { - let indexLookup = this.keyIndexPrefix + JSON.stringify(key); - near.storageRemove(indexLookup); + // Set instead of remove to avoid loading the value from storage. + this.values.set(key, null); } this.keys.clear(); - this.values.clear(); } toArray() { let ret = []; @@ -133,28 +85,28 @@ export class UnorderedMap { // converting plain object to class object static deserialize(data) { let map = new UnorderedMap(data.prefix); - // reconstruct UnorderedMap - map.length = data.length; // reconstruct keys Vector - map.keys = new Vector(data.prefix + "k"); + map.keys = new Vector(data.prefix + "u"); map.keys.length = data.keys.length; - // reconstruct values Vector - map.values = new Vector(data.prefix + "v"); - map.values.length = data.values.length; + // reconstruct values LookupMap + map.values = new LookupMap(data.prefix + "m"); return map; } } class UnorderedMapIterator { constructor(unorderedMap) { this.keys = new VectorIterator(unorderedMap.keys); - this.values = new VectorIterator(unorderedMap.values); + this.map = unorderedMap.values; } next() { let key = this.keys.next(); - let value = this.values.next(); - if (key.done != value.done) { - throw new Error(ERR_INCONSISTENT_STATE); + let value; + if (!key.done) { + value = this.map.get(key.value); + if (value === null) { + throw new Error(ERR_INCONSISTENT_STATE); + } } - return { value: [key.value, value.value], done: key.done }; + return { value: [key.value, value ? value[0] : value], done: key.done }; } } diff --git a/lib/collections/unordered-set.d.ts b/lib/collections/unordered-set.d.ts index 7989a1ba..1cdf48b6 100644 --- a/lib/collections/unordered-set.d.ts +++ b/lib/collections/unordered-set.d.ts @@ -6,7 +6,6 @@ export declare class UnorderedSet { readonly elements: Vector; constructor(prefix: Bytes); get length(): number; - private set length(value); isEmpty(): boolean; contains(element: unknown): boolean; set(element: unknown): boolean; diff --git a/lib/collections/unordered-set.js b/lib/collections/unordered-set.js index fc34e49a..948bb67f 100644 --- a/lib/collections/unordered-set.js +++ b/lib/collections/unordered-set.js @@ -22,8 +22,6 @@ export class UnorderedSet { get length() { return this.elements.length; } - // noop, called by deserialize - set length(_l) { } isEmpty() { return this.elements.isEmpty(); } @@ -102,8 +100,6 @@ export class UnorderedSet { // converting plain object to class object static deserialize(data) { let set = new UnorderedSet(data.prefix); - // reconstruct UnorderedSet - set.length = data.length; // reconstruct Vector let elementsPrefix = data.prefix + "e"; set.elements = new Vector(elementsPrefix); diff --git a/src/collections/unordered-map.ts b/src/collections/unordered-map.ts index 6e902c17..ae60e2b0 100644 --- a/src/collections/unordered-map.ts +++ b/src/collections/unordered-map.ts @@ -1,131 +1,86 @@ -import * as near from "../api"; -import { u8ArrayToBytes, bytesToU8Array, Bytes, Mutable } from "../utils"; +import { Bytes, Mutable } from "../utils"; import { Vector, VectorIterator } from "./vector"; +import { LookupMap } from "./lookup-map"; const ERR_INCONSISTENT_STATE = "The collection is an inconsistent state. Did previous smart contract execution terminate unexpectedly?"; -function serializeIndex(index: number): Bytes { - let data = new Uint32Array([index]); - let array = new Uint8Array(data.buffer); - return u8ArrayToBytes(array); -} - -function deserializeIndex(rawIndex: Bytes) { - let array = bytesToU8Array(rawIndex); - let data = new Uint32Array(array.buffer); - return data[0]; -} - -function getIndexRaw(keyIndexPrefix: Bytes, key: Bytes): Bytes { - let indexLookup = keyIndexPrefix + JSON.stringify(key); - let indexRaw = near.storageRead(indexLookup); - return indexRaw; -} +type ValueAndIndex = [value: unknown, index: number] export class UnorderedMap { readonly prefix: Bytes; - readonly keyIndexPrefix: Bytes; readonly keys: Vector; - readonly values: Vector; + readonly values: LookupMap; constructor(prefix: Bytes) { this.prefix = prefix; - this.keyIndexPrefix = prefix + "i"; - let indexKey = prefix + "k"; - let indexValue = prefix + "v"; - this.keys = new Vector(indexKey); - this.values = new Vector(indexValue); + this.keys = new Vector(prefix + 'u'); // intentional different prefix with old UnorderedMap + this.values = new LookupMap(prefix + 'm'); } get length() { let keysLen = this.keys.length; - let valuesLen = this.values.length; - if (keysLen != valuesLen) { - throw new Error(ERR_INCONSISTENT_STATE); - } return keysLen; } - // noop, called by deserialize - private set length(_l: number) {} - isEmpty(): boolean { let keysIsEmpty = this.keys.isEmpty(); - let valuesIsEmpty = this.values.isEmpty(); - if (keysIsEmpty != valuesIsEmpty) { - throw new Error(ERR_INCONSISTENT_STATE); - } return keysIsEmpty; } get(key: Bytes): unknown | null { - let indexRaw = getIndexRaw(this.keyIndexPrefix, key); - if (indexRaw) { - let index = deserializeIndex(indexRaw); - let value = this.values.get(index); - if (value) { - return value; - } else { - throw new Error(ERR_INCONSISTENT_STATE); - } + let valueAndIndex = this.values.get(key); + if (valueAndIndex === null) { + return null; } - return null; + let value = (valueAndIndex as ValueAndIndex)[0]; + return value; } set(key: Bytes, value: unknown): unknown | null { - let indexLookup = this.keyIndexPrefix + JSON.stringify(key); - let indexRaw = near.storageRead(indexLookup); - if (indexRaw) { - let index = deserializeIndex(indexRaw); - return this.values.replace(index, value); - } else { - let nextIndex = this.length; - let nextIndexRaw = serializeIndex(nextIndex); - near.storageWrite(indexLookup, nextIndexRaw); - this.keys.push(key); - this.values.push(value); - return null; + let valueAndIndex = this.values.get(key); + if (valueAndIndex !== null) { + let oldValue = (valueAndIndex as ValueAndIndex)[0]; + (valueAndIndex as ValueAndIndex)[0] = value; + this.values.set(key, valueAndIndex) + return oldValue; } + + let nextIndex = this.length; + this.keys.push(key); + this.values.set(key, [value, nextIndex]); + return null; } remove(key: Bytes): unknown | null { - let indexLookup = this.keyIndexPrefix + JSON.stringify(key); - let indexRaw = near.storageRead(indexLookup); - if (indexRaw) { - if (this.length == 1) { - // If there is only one element then swap remove simply removes it without - // swapping with the last element. - near.storageRemove(indexLookup); - } else { - // If there is more than one element then swap remove swaps it with the last - // element. - let lastKey = this.keys.get(this.length - 1); - if (!lastKey) { - throw new Error(ERR_INCONSISTENT_STATE); - } - near.storageRemove(indexLookup); - // If the removed element was the last element from keys, then we don't need to - // reinsert the lookup back. - if (lastKey != key) { - let lastLookupKey = this.keyIndexPrefix + JSON.stringify(lastKey); - near.storageWrite(lastLookupKey, indexRaw); - } + let oldValueAndIndex = this.values.remove(key); + if (oldValueAndIndex === null) { + return null; + } + let index = (oldValueAndIndex as ValueAndIndex)[1]; + if (this.keys.swapRemove(index) === null) { + throw new Error(ERR_INCONSISTENT_STATE); + } + + // the last key is swapped to key[index], the corresponding [value, index] need update + if (this.keys.length > 0 && index != this.keys.length) { + // if there is still elements and it was not the last element + let swappedKey = this.keys.get(index) as Bytes; + let swappedValueAndIndex = this.values.get(swappedKey); + if (swappedValueAndIndex === null) { + throw new Error(ERR_INCONSISTENT_STATE) } - let index = deserializeIndex(indexRaw); - this.keys.swapRemove(index); - return this.values.swapRemove(index); + this.values.set(swappedKey, [swappedValueAndIndex[0], index]) } - return null; + return (oldValueAndIndex as ValueAndIndex)[0]; } clear() { for (let key of this.keys) { - let indexLookup = this.keyIndexPrefix + JSON.stringify(key); - near.storageRemove(indexLookup); + // Set instead of remove to avoid loading the value from storage. + this.values.set(key as Bytes, null); } this.keys.clear(); - this.values.clear(); } toArray(): [Bytes, unknown][] { @@ -155,32 +110,33 @@ export class UnorderedMap { // removing readonly modifier type MutableUnorderedMap = Mutable; let map = new UnorderedMap(data.prefix) as MutableUnorderedMap; - // reconstruct UnorderedMap - map.length = data.length; // reconstruct keys Vector - map.keys = new Vector(data.prefix + "k"); + map.keys = new Vector(data.prefix + "u"); map.keys.length = data.keys.length; - // reconstruct values Vector - map.values = new Vector(data.prefix + "v"); - map.values.length = data.values.length; + // reconstruct values LookupMap + map.values = new LookupMap(data.prefix + "m"); return map as UnorderedMap; } } class UnorderedMapIterator { private keys: VectorIterator; - private values: VectorIterator; + private map: LookupMap; + constructor(unorderedMap: UnorderedMap) { this.keys = new VectorIterator(unorderedMap.keys); - this.values = new VectorIterator(unorderedMap.values); + this.map = unorderedMap.values; } next(): { value: [unknown | null, unknown | null]; done: boolean } { let key = this.keys.next(); - let value = this.values.next(); - if (key.done != value.done) { - throw new Error(ERR_INCONSISTENT_STATE); + let value; + if (!key.done) { + value = this.map.get(key.value as Bytes); + if (value === null) { + throw new Error(ERR_INCONSISTENT_STATE); + } } - return { value: [key.value, value.value], done: key.done }; + return { value: [key.value, value ? value[0] : value], done: key.done }; } } diff --git a/src/collections/unordered-set.ts b/src/collections/unordered-set.ts index eefc3725..49c0ca82 100644 --- a/src/collections/unordered-set.ts +++ b/src/collections/unordered-set.ts @@ -34,9 +34,6 @@ export class UnorderedSet { return this.elements.length; } - // noop, called by deserialize - private set length(_l: number) {} - isEmpty(): boolean { return this.elements.isEmpty(); } @@ -124,8 +121,6 @@ export class UnorderedSet { // removing readonly modifier type MutableUnorderedSet = Mutable; let set = new UnorderedSet(data.prefix) as MutableUnorderedSet; - // reconstruct UnorderedSet - set.length = data.length; // reconstruct Vector let elementsPrefix = data.prefix + "e"; set.elements = new Vector(elementsPrefix);