/**
* @author Michael Hasenstein <hasenstein@yahoo.com>
* @copyright REFINIO GmbH 2017
* @license CC-BY-NC-SA-2.5; portions MIT License
* @version 0.0.1
*/
/**
* System-internal version maps for versioned objects are CSV files with LF (Unix style newline)
* line endings and comma-separated pairs of timestamp (left-padded with "0"s to always use 16
* characters), object-hash. All object hashes in a given version map are versions of the same
* ID object.
*
* Since we only append to the file the latest version can be found at the very end, and the
* order is a chronological log of version creation.
*
* Format:
*
* ```
* timestamp,objectHash
* timestamp,objectHash
* timestamp,objectHash
* ...
* ```
*
* The `timestamp` is the time when the version map entry was made.
*
* @module
*/
import type {OneVersionedObjectTypes} from './recipes';
import {STORAGE} from './storage-base-common';
import {readTextFileSection, readUTF8TextFile} from './system/storage-base';
import {calculateIdHashOfObj} from './util/object';
import {serializeWithType} from './util/promise';
import {substrForceMemCopy} from './util/string';
import type {SHA256Hash, SHA256IdHash} from './util/type-checks';
import {ensureHash} from './util/type-checks';
import type {VersionMapEntry} from './version-map-updater';
import {TIMESTAMP_LENGTH} from './version-map-updater';
/**
* Each entry is "timestamp,hash\n" where the timestamp is a fixed-length string and the SHA-256
* hash is 64 (hexadecimal) characters long.
* Total: TIMESTAMP_LENGTH + 1 + 64 + 1
* @private
* @type {number}
*/
const LINE_LENGTH = TIMESTAMP_LENGTH + 66;
/**
* @private
* @param {SHA256IdHash} idHash - The filename of the map to be loaded, which is the ID hash of a
* versioned object
* @param {number} [index=-1] - The zero-based integer index (0, 1,...) in the version map from
* which the object hash should be returned. If the index is *negative* the count starts at
* the end of the list of versions. For example, to get the latest entry you can use an index of
* -1, or -2 for the last but one, etc. If no index is given the default is -1 for the latest
* version.
* @returns {Promise<string>} Returns a promise resolving with the n-th entry (line) in the version
* map. THERE ARE NO SANITY CHECKS: We expect this data loaded from storage to be correct.
*/
function getNthLineSerialized(idHash: SHA256IdHash, index: number = -1): Promise<string> {
return serializeWithType('VersionMap ' + idHash, () =>
readTextFileSection(idHash, index * LINE_LENGTH, LINE_LENGTH, STORAGE.VMAPS)
);
}
/**
* @static
* @async
* @param {SHA256IdHash} idHash - The filename of the map to be loaded, which is the ID hash of a
* versioned object
* @param {number} [index=-1] - The zero-based integer index (0, 1,...) in the version map from
* which the object hash should be returned. If the index is *negative* the count starts at
* the end of the list of versions. For example, to get the latest entry you can use an index of
* -1, or -2 for the last but one, etc. If no index is given the default is -1 for the latest
* version.
* @returns {Promise<SHA256Hash>} Returns a promise resolving with the SHA-256 object hash stored
* for the given index
*/
export async function getNthVersionMapHash<T extends OneVersionedObjectTypes>(
idHash: SHA256IdHash<T>,
index: number = -1
): Promise<SHA256Hash<T>> {
// Line format:
// timestamp,object-hash\n
const entry = await getNthLineSerialized(idHash, index);
return ensureHash(substrForceMemCopy(entry, TIMESTAMP_LENGTH + 1, 64));
}
/**
* @static
* @async
* @param {SHA256IdHash} idHash - The filename of the map to be loaded, which is the ID hash of a
* versioned object
* @param {number} [index=-1] - The zero-based integer index (0, 1,...) in the version map from
* which the object hash should be returned. If the index is *negative* the count starts at
* the end of the list of versions. For example, to get the latest entry you can use an index of
* -1, or -2 for the last but one, etc. If no index is given the default is -1 for the latest
* version.
* @returns {Promise<VersionMapEntry>} Returns a VersionMapEntry object
*/
export async function getNthVersionMapEntry<T extends OneVersionedObjectTypes>(
idHash: SHA256IdHash<T>,
index: number = -1
): Promise<VersionMapEntry<T>> {
// timestamp,object-hash\n
const entry = await getNthLineSerialized(idHash, index);
return {
timestamp: +entry.substr(0, TIMESTAMP_LENGTH),
hash: ensureHash(substrForceMemCopy(entry, TIMESTAMP_LENGTH + 1, 64))
};
}
/**
* Reads the version map with the given ID hash
* @static
* @async
* @param {SHA256IdHash} idHash - The filename of the map to be loaded, which is the ID hash of a
* versioned object
* @returns {Promise<VersionMapEntry[]>} Returns a promise resolving with an array
* of {@link VersionMapEntry} objects
*/
export function getAllVersionMapEntries<T extends OneVersionedObjectTypes>(
idHash: SHA256IdHash<T>
): Promise<Array<VersionMapEntry<T>>> {
async function getEntries(): Promise<Array<VersionMapEntry<T>>> {
const mapData = await readUTF8TextFile(idHash, STORAGE.VMAPS);
return mapData
.slice(0, -1) // Remove the final newline character
.split('\n')
.map(entry => ({
timestamp: +entry.substr(0, TIMESTAMP_LENGTH),
hash: ensureHash(substrForceMemCopy(entry, TIMESTAMP_LENGTH + 1, 64))
}));
}
return serializeWithType('VersionMap ' + idHash, getEntries);
}
/**
* Reads the version map with the given ID hash
* @static
* @async
* @param {SHA256IdHash} idHash - The filename of the map to be loaded, which is the ID hash of a
* versioned object
* @param {SHA256Hash} hash - The hash under the given ID hash for which the creation timestamp
* should be returned.
* @returns {Promise<number|undefined>} Returns a promise resolving with the timestamp of the
* given hash under the given ID hash, or `undefined` if no entry was found for the given hash
* in the given version map identified by the given ID hash.
*/
export function getTimestampForHash<T extends OneVersionedObjectTypes>(
idHash: SHA256IdHash<T>,
hash: SHA256Hash<T>
): Promise<number | void> {
async function getTimestamp(): Promise<number | void> {
const mapData = await readUTF8TextFile(idHash, STORAGE.VMAPS);
// Start from the end to get the latest, instead of the first, entry for that hash
let pos = mapData.length;
while (pos > 0) {
// Each entry is: "timestamp,hash\n"
pos -= TIMESTAMP_LENGTH + 66;
if (mapData.substr(pos + TIMESTAMP_LENGTH + 1, 64) === hash) {
return +mapData.substr(pos, TIMESTAMP_LENGTH);
}
}
}
return serializeWithType('VersionMap ' + idHash, getTimestamp);
}
/**
* Frontend for
* {@link storage.module:ts.getAllVersionMapEntries|`getAllVersionMapEntries`} that accepts an ID
* object and calculates its crypto hash before calling that function. This function allows to
* retrieve a list of available versions for a given object ID. It returns an array of numbers
* (dates in milliseconds since 1/1/1970) and object hashes which can then be used to retrieve a
* specific version.
* @static
* @async
* @param {OneVersionedObjectTypes} obj - A versioned ONE object which is used to create its ID
* hash. Note that it does not have to be an ID object, i.e. it can have more properties than
* just ID properties, they will be ignored.
* @returns {Promise<VersionMapEntry[]>} An array of {@link VersionMapEntry} objects
*/
export async function listVersionsByIdObj(
obj: Readonly<OneVersionedObjectTypes>
): Promise<VersionMapEntry[]> {
const idHash = await calculateIdHashOfObj(obj);
return await getAllVersionMapEntries(idHash);
}