interface-datastore
Version:
439 lines (395 loc) • 9.9 kB
text/typescript
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import type { SupportedEncodings } from 'uint8arrays/to-string'
const pathSepS = '/'
const pathSepB = new TextEncoder().encode(pathSepS)
const pathSep = pathSepB[0]
/**
* A Key represents the unique identifier of an object.
* Our Key scheme is inspired by file systems and Google App Engine key model.
* Keys are meant to be unique across a system. Keys are hierarchical,
* incorporating more and more specific namespaces. Thus keys can be deemed
* 'children' or 'ancestors' of other keys:
* - `new Key('/Comedy')`
* - `new Key('/Comedy/MontyPython')`
* Also, every namespace can be parametrized to embed relevant object
* information. For example, the Key `name` (most specific namespace) could
* include the object type:
* - `new Key('/Comedy/MontyPython/Actor:JohnCleese')`
* - `new Key('/Comedy/MontyPython/Sketch:CheeseShop')`
* - `new Key('/Comedy/MontyPython/Sketch:CheeseShop/Character:Mousebender')`
*
*/
export class Key {
private _buf: Uint8Array
/**
* @param {string | Uint8Array} s
* @param {boolean} [clean]
*/
constructor (s: string | Uint8Array, clean?: boolean) {
if (typeof s === 'string') {
this._buf = uint8ArrayFromString(s)
} else if (s instanceof Uint8Array) {
this._buf = s
} else {
throw new Error('Invalid key, should be String of Uint8Array')
}
if (clean == null) {
clean = true
}
if (clean) {
this.clean()
}
if (this._buf.byteLength === 0 || this._buf[0] !== pathSep) {
throw new Error('Invalid key')
}
}
/**
* Convert to the string representation
*
* @param {import('uint8arrays/to-string').SupportedEncodings} [encoding] - The encoding to use.
* @returns {string}
*/
toString (encoding: SupportedEncodings = 'utf8'): string {
return uint8ArrayToString(this._buf, encoding)
}
/**
* Return the Uint8Array representation of the key
*
* @returns {Uint8Array}
*/
uint8Array (): Uint8Array {
return this._buf
}
/**
* Return string representation of the key
*
* @returns {string}
*/
get [Symbol.toStringTag] (): string {
return `Key(${this.toString()})`
}
/**
* Constructs a key out of a namespace array.
*
* @param {Array<string>} list - The array of namespaces
* @returns {Key}
*
* @example
* ```js
* Key.withNamespaces(['one', 'two'])
* // => Key('/one/two')
* ```
*/
static withNamespaces (list: string[]): Key {
return new Key(list.join(pathSepS))
}
/**
* Returns a randomly (uuid) generated key.
*
* @returns {Key}
*
* @example
* ```js
* Key.random()
* // => Key('/344502982398')
* ```
*/
static random (): Key {
return new Key(Math.random().toString().substring(2))
}
/**
* @param {*} other
*/
static asKey (other: any): Key | null {
if (other instanceof Uint8Array || typeof other === 'string') {
// we can create a key from this
return new Key(other)
}
if (typeof other.uint8Array === 'function') {
// this is an older version or may have crossed the esm/cjs boundary
return new Key(other.uint8Array())
}
return null
}
/**
* Cleanup the current key
*
* @returns {void}
*/
clean (): void {
if (this._buf == null || this._buf.byteLength === 0) {
this._buf = pathSepB
}
if (this._buf[0] !== pathSep) {
const bytes = new Uint8Array(this._buf.byteLength + 1)
bytes.fill(pathSep, 0, 1)
bytes.set(this._buf, 1)
this._buf = bytes
}
// normalize does not remove trailing slashes
while (this._buf.byteLength > 1 && this._buf[this._buf.byteLength - 1] === pathSep) {
this._buf = this._buf.subarray(0, -1)
}
}
/**
* Check if the given key is sorted lower than ourself.
*
* @param {Key} key - The other Key to check against
* @returns {boolean}
*/
less (key: Key): boolean {
const list1 = this.list()
const list2 = key.list()
for (let i = 0; i < list1.length; i++) {
if (list2.length < i + 1) {
return false
}
const c1 = list1[i]
const c2 = list2[i]
if (c1 < c2) {
return true
} else if (c1 > c2) {
return false
}
}
return list1.length < list2.length
}
/**
* Returns the key with all parts in reversed order.
*
* @returns {Key}
*
* @example
* ```js
* new Key('/Comedy/MontyPython/Actor:JohnCleese').reverse()
* // => Key('/Actor:JohnCleese/MontyPython/Comedy')
* ```
*/
reverse (): Key {
return Key.withNamespaces(this.list().slice().reverse())
}
/**
* Returns the `namespaces` making up this Key.
*
* @returns {Array<string>}
*/
namespaces (): string[] {
return this.list()
}
/**
* Returns the "base" namespace of this key.
*
* @returns {string}
*
* @example
* ```js
* new Key('/Comedy/MontyPython/Actor:JohnCleese').baseNamespace()
* // => 'Actor:JohnCleese'
* ```
*/
baseNamespace (): string {
const ns = this.namespaces()
return ns[ns.length - 1]
}
/**
* Returns the `list` representation of this key.
*
* @returns {Array<string>}
*
* @example
* ```js
* new Key('/Comedy/MontyPython/Actor:JohnCleese').list()
* // => ['Comedy', 'MontyPythong', 'Actor:JohnCleese']
* ```
*/
list (): string[] {
return this.toString().split(pathSepS).slice(1)
}
/**
* Returns the "type" of this key (value of last namespace).
*
* @returns {string}
*
* @example
* ```js
* new Key('/Comedy/MontyPython/Actor:JohnCleese').type()
* // => 'Actor'
* ```
*/
type (): string {
return namespaceType(this.baseNamespace())
}
/**
* Returns the "name" of this key (field of last namespace).
*
* @returns {string}
*
* @example
* ```js
* new Key('/Comedy/MontyPython/Actor:JohnCleese').name()
* // => 'JohnCleese'
* ```
*/
name (): string {
return namespaceValue(this.baseNamespace())
}
/**
* Returns an "instance" of this type key (appends value to namespace).
*
* @param {string} s - The string to append.
* @returns {Key}
*
* @example
* ```js
* new Key('/Comedy/MontyPython/Actor').instance('JohnClesse')
* // => Key('/Comedy/MontyPython/Actor:JohnCleese')
* ```
*/
instance (s: string): Key {
return new Key(this.toString() + ':' + s)
}
/**
* Returns the "path" of this key (parent + type).
*
* @returns {Key}
*
* @example
* ```js
* new Key('/Comedy/MontyPython/Actor:JohnCleese').path()
* // => Key('/Comedy/MontyPython/Actor')
* ```
*/
path (): Key {
let p = this.parent().toString()
if (!p.endsWith(pathSepS)) {
p += pathSepS
}
p += this.type()
return new Key(p)
}
/**
* Returns the `parent` Key of this Key.
*
* @returns {Key}
*
* @example
* ```js
* new Key("/Comedy/MontyPython/Actor:JohnCleese").parent()
* // => Key("/Comedy/MontyPython")
* ```
*/
parent (): Key {
const list = this.list()
if (list.length === 1) {
return new Key(pathSepS)
}
return new Key(list.slice(0, -1).join(pathSepS))
}
/**
* Returns the `child` Key of this Key.
*
* @param {Key} key - The child Key to add
* @returns {Key}
*
* @example
* ```js
* new Key('/Comedy/MontyPython').child(new Key('Actor:JohnCleese'))
* // => Key('/Comedy/MontyPython/Actor:JohnCleese')
* ```
*/
child (key: Key): Key {
if (this.toString() === pathSepS) {
return key
} else if (key.toString() === pathSepS) {
return this
}
return new Key(this.toString() + key.toString(), false)
}
/**
* Returns whether this key is a prefix of `other`
*
* @param {Key} other - The other key to test against
* @returns {boolean}
*
* @example
* ```js
* new Key('/Comedy').isAncestorOf('/Comedy/MontyPython')
* // => true
* ```
*/
isAncestorOf (other: Key): boolean {
if (other.toString() === this.toString()) {
return false
}
return other.toString().startsWith(this.toString())
}
/**
* Returns whether this key is a contains another as prefix.
*
* @param {Key} other - The other Key to test against
* @returns {boolean}
*
* @example
* ```js
* new Key('/Comedy/MontyPython').isDecendantOf('/Comedy')
* // => true
* ```
*/
isDecendantOf (other: Key): boolean {
if (other.toString() === this.toString()) {
return false
}
return this.toString().startsWith(other.toString())
}
/**
* Checks if this key has only one namespace.
*
* @returns {boolean}
*/
isTopLevel (): boolean {
return this.list().length === 1
}
/**
* Concats one or more Keys into one new Key.
*
* @param {Array<Key>} keys - The array of keys to concatenate
* @returns {Key}
*/
concat (...keys: Key[]): Key {
return Key.withNamespaces([...this.namespaces(), ...flatten(keys.map(key => key.namespaces()))])
}
}
/**
* The first component of a namespace. `foo` in `foo:bar`
*
* @param {string} ns
* @returns {string}
*/
function namespaceType (ns: string): string {
const parts = ns.split(':')
if (parts.length < 2) {
return ''
}
return parts.slice(0, -1).join(':')
}
/**
* The last component of a namespace, `baz` in `foo:bar:baz`.
*
* @param {string} ns
* @returns {string}
*/
function namespaceValue (ns: string): string {
const parts = ns.split(':')
return parts[parts.length - 1]
}
/**
* Flatten array of arrays (only one level)
*
* @template T
* @param {Array<any>} arr
* @returns {T[]}
*/
function flatten (arr: any[]): string[] {
return ([]).concat(...arr)
}