/**
* @author Michael Hasenstein <hasenstein@yahoo.com>
* @copyright REFINIO GmbH 2017
* @license CC-BY-NC-SA-2.5; portions MIT License
* @version 0.0.1
*/
/**
* This module contains functions and definitions for the base storage module functions that are
* shared across all platforms.
* @module
*/
/**
* Options for the {@link initStorage} function in module `system/storage-base`. The function is
* called by `initInstance`.
* @global
* @typedef {object} InitStorageOptions
* @param {SHA256IdHash} instanceIdHash
* @param {boolean} [wipeStorage]
* @param {string} [name]
* @param {number} [nHashCharsForSubDirs]
* @param {number} [storageInitTimeout]
* @param {boolean} [encryptStorage]
* @param {(string|null)} secretForStorageKey
*/
export interface InitStorageOptions {
instanceIdHash: SHA256IdHash<Instance>;
wipeStorage?: boolean;
name?: string;
nHashCharsForSubDirs?: number;
storageInitTimeout?: number;
encryptStorage?: boolean;
secretForStorageKey: string | null;
}
/**
* The settings store is a key-value store used to store instance settings that cannot or should
* not be stored in the usual ONE storage places.
* The browser implementation uses `localStorage` and the node.js version stores a file with the
* settings ina JSON.stringified object in the "private" storage space.
* @global
* @typedef {object} SettingStoreApi
* @property {function(string):Promise<string|AnyObject|undefined>} getItem
* @property {function(string,(string|AnyObject)):Promise<undefined>} setItem
* @property {function(string):Promise<undefined>} removeItem
* @property {function():Promise<undefined>} clear
*/
export interface SettingStoreApi {
getItem: (key: string) => Promise<string | AnyObject | undefined>;
setItem: (key: string, value: string | AnyObject) => Promise<void>;
removeItem: (key: string) => Promise<void>;
clear: () => Promise<void>;
}
/**
* `SystemReadStream` objects are created by factory function
* {@link system/storage-streams.module:ts.createFileReadStream|system/storage-streams.createFileReadStream}.
*
* This type has a generic parameter for the encoding setting: `undefined` | 'utf8' | 'base64'.
* Depending on the encoding the `ondData` function yields `string` or `ArrayBuffer` data.
*
* It provides a platform independent interface to streams: filesystem streams on node.js,
* {@link https://github.com/wkh237/react-native-fetch-blob|RNFetchBlob} on React Native, and a
* custom minimal implementation on browsers.
*
* The event system is modeled after the one found for IndexedDB and for WebSockets, among
* others: You simply assign a function to the appropriately named property on the
* `SimpleReadStream` object.
* @global
* @typedef {object} SimpleReadStream
* @property {('base64'|'utf8')} [encoding] - `undefined` for ArrayBuffer based binary streams,
* "`base64"` for string based binary streams, "`utf8"` for UTF-8 string based text streams. In
* the ONE library context:
* Requests for BLOBs: `undefined` (normal case) or `base64` (React Native client)
* Requests for CLOBs and ONE objects: `utf8`
* @property {OneEventSourceConsumer<string|ArrayBuffer>} onData - Event source property with its
* public methods to subscribe to events
* @property {function():void} pause - Pause the read stream; `() => void`
* @property {function():void} resume - Resume the read stream; `() => void`
* @property {function():void} cancel - Cancel the read stream; `() => void`
* @property {Promise<void>} promise - A
* {@link util/promise.module:ts.createTrackingPromise|tracking promise}
* for 3rd parties to get the result when the stream ends (stream completed or error)
*/
export interface SimpleReadStream<
E extends undefined | 'base64' | 'utf8' = undefined | 'base64' | 'utf8'
> {
encoding: E;
onData: OneEventSourceConsumer<E extends undefined ? ArrayBuffer : string>;
pause: () => void;
resume: () => void;
cancel: () => void;
promise: Promise<void>;
}
/**
* `SimpleWriteStream` objects are created by factory function
* {@link system/storage-streams.module:ts.createFileWriteStream|storage-streams.createFileWriteStream}.
*
* This type has a generic parameter for the encoding setting: `undefined` | 'utf8' | 'base64'.
* Depending on the encoding the `write` function expects (only) `string` or `ArrayBuffer` data.
*
* It provides a platform independent interface to streams: filesystem streams on node.js,
* {@link https://github.com/wkh237/react-native-fetch-blob|RNFetchBlob} on React Native, and a
* custom minimal implementation on browsers.
*
* Instead of events for "error" and "finish" there is a `promise`, which allows for easier
* integration with promise based code in general and async/await based code in particular.
*
* ## Style
*
* This write-stream object assumes two different kinds of clients:
*
* - A single instance of code that actually uses and manages the stream
*
* - An arbitrary number of instances of code not involved with the stream but interested in its
* final result.
*
* The active code gets functions to control the stream.
*
* Passively involved code gets a promise that will resolve or reject when the final result of
* the stream becomes available. That promise does not control any of the code used to run the
* stream, it is only used to communicate the official result.
*
* The code actually using the stream would have no use for a promise though: Streams and
* promises are fundamentally different in their aims. The code that *does* end up using the
* promise, however, a promise is the perfect abstraction - it only wants to know the final result.
* @global
* @typedef {object} SimpleWriteStream
* @property {function((string|ArrayBuffer)):void} write - `(data: E extends undefined ?
* ArrayBuffer :
* string) => void` - If the write-stream was created without an encoding the parameter must be
* an ArrayBuffer. If it was created with an encoding there must be a string, either UTF-8 or a
* Base64 encoded binary, depending on the encoding.
* @property {function():Promise<undefined>} cancel - `() => Promise<void>` - Cancel stream and
* remove temporary file
* @property {function():Promise<FileCreation>} end - `() => Promise<FileCreation<E extends
* 'utf8' ? CLOB : BLOB>>` - Finish the stream, return final result
* @property {Promise<FileCreation<'BLOB'>>} promise - A
* {@link util/promise.module:ts.createTrackingPromise|tracking promise}
* for 3rd parties to get the result when the stream ends (stream completed or error)
*/
export interface SimpleWriteStream<
E extends undefined | 'base64' | 'utf8' = undefined | 'base64' | 'utf8'
> {
write: (data: E extends undefined ? ArrayBuffer : string) => void;
cancel: () => Promise<void>;
end: () => Promise<FileCreation<E extends 'utf8' ? CLOB : BLOB>>;
promise: Promise<FileCreation<E extends 'utf8' ? CLOB : BLOB>>;
}
/**
* A union type of versioned and unversioned object creation result objects, for any ONE object
* type.
* @global
* @typedef {(UnversionedObjectResult|VersionedObjectResult)} AnyObjectCreation
*/
export type AnyObjectCreation<T extends OneObjectTypes = OneObjectTypes> =
| UnversionedObjectResult<T extends OneUnversionedObjectTypes ? T : never>
| VersionedObjectResult<T extends OneVersionedObjectTypes ? T : never>;
/**
* This defines the creation status string constants for files such as ONE microdata files,
* CLOBs and BLOBs. This overlaps with the status for unversioned ONE objects because those are
* simple text files for the low-level storage API that does not deal with ONE objects but
* simple with "files" in the most general sense.
*
* To check a status don't use the string constants directly (they could be changed!). Use
* storage module's exported `CREATION_STATUS` enum (static frozen object) with the properties
* `NEW` and `EXISTS`.
*
* ### Usage
*
* Instead of using these strings directly please use the
* {@link storage-base-common.module:ts.CREATION_STATUS|storage-base-common.CREATION_STATUS}
* export of
*
* ```javascript
* import {StorageBaseCommon} from 'storage-base-common');
*
* // StorageBaseCommon.CREATION_STATUS.NEW
* // StorageBaseCommon.CREATION_STATUS.EXISTS
* ```
* @global
* @typedef {("new"|"exists")} FileCreationStatus
*/
export type FileCreationStatus = (typeof CREATION_STATUS)[keyof typeof CREATION_STATUS];
/**
* Return result object of creating CLOB and BLOB file objects.
* @global
* @typedef {object} FileCreation
* @property {SHA256Hash} hash - The SHA-256 hash of the contents of a versioned ONE object
* @property {FileCreationStatus} status - A string constant showing whether the file
* already existed or if it had to be created.
*/
export interface FileCreation<T extends HashTypes> {
hash: SHA256Hash<T>;
status: FileCreationStatus;
}
import type {
BLOB,
CLOB,
HashTypes,
Instance,
OneObjectTypes,
OneUnversionedObjectTypes,
OneVersionedObjectTypes
} from './recipes';
import type {UnversionedObjectResult} from './storage-unversioned-objects';
import type {VersionedObjectResult} from './storage-versioned-objects';
import type {AnyObject} from './util/object';
import type {OneEventSourceConsumer} from './util/one-event-source';
import type {SHA256Hash, SHA256IdHash} from './util/type-checks';
/**
* These static strings describe object creation. If an object did not exist yet - for versioned
* objects that includes any previous versions based on the ID-object - the status is "new". If
* an object that is to be created already exists (which is recognized because the names of all
* files are based on the SHA256 crypto hash of its contents) the status is "exists". For
* versioned objects this means the exact object already exists, not just a previous version
* (base don ID-object). The last state is used for versioned objects only. When a previous
* version of the object exists, based on the ID-object, but the exact version of the object
* does not, the object is created and added as a new version of the existing ID-object.
* *Note:* This is defined here and not in storage-unversioned-objects.js because then we
* would have to import that file here, but since we already do it the other way around it's
* easier to avoid a cyclic reference for such a minor thing, even though it works. **Always
* use the names (keys) on this structure, never use the values themselves!**
* @static
* @type {object}
* @property {'new'} NEW - There was not even a previous version of this object
* @property {'exists'} EXISTS - This exact object (identified by SHA-256) already exists
*/
export const CREATION_STATUS = {
NEW: 'new',
EXISTS: 'exists'
} as const;
/**
* String constants for {@link SetAccessParam}'s `mode` parameter.
* @static
* @type {object}
* @property {'replace'} REPLACE
* @property {'add'} ADD
*/
export const SET_ACCESS_MODE = {
REPLACE: 'replace',
ADD: 'add'
} as const;
/**
* String constants for the storage types.
* Avoid having to repeat the string constant
* @static
* @type {object}
* @property {OBJECTS} "objects"
* @property {PRIVATE} "private"
*/
export const STORAGE = {
OBJECTS: 'objects',
TMP: 'tmp',
RMAPS: 'rmaps',
VMAPS: 'vmaps',
ACACHE: 'acache',
PRIVATE: 'private'
} as const;
/**
* One of these fixed strings: `'objects' | 'tmp' | rmaps | vmaps | 'acache', 'private'`
* @global
* @typedef {'objects'|'tmp'|'rmaps'|'vmaps'|'acache'|'private'} StorageDirTypes
*/
export type StorageDirTypes = (typeof STORAGE)[keyof typeof STORAGE];
/**
* Used to ensure createTempFileName() creates unique names.
* @private
* @type {number}
*/
let tempFileNameCounter = 0;
/**
* Temporary filenames are needed for files we receive as streams, for example BLOBs received
* during Chum exchange. We won't know the SHA-256 normally used as filename until we received it
* completely.
*
* **NOTE:** This function cannot be replaced by system/crypto-helpers createRandomString. We
* need this to be a synchronous function, or we would have to return SimpleWriteStream API
* objects asynchronously (undesirable because illogical).
* @static
* @returns {string} A randomly created temporary filename
*/
export function createTempFileName(): string {
// Using only the counter is sufficient because each instance, identified by its ID hash,
// has its own storage space and in it its own "tmp" area. One would have to run two
// versions of the same instance at the same time to get conflicts. Still, in case of
// conflict with leftover files from a previous run we also use a timestamp. Also using the
// counter avoids conflicts for multiple calls to this function within the same millisecond.
// While there should not be any leftover tmp files from previous runs that is a higher
// level issue that in this function we don't want to rely on, because that might very well
// change. So let's just create a name that will still work if name conflicts across
// multiple runs are possible, since it is so easy to avoid and since it removes a very
// low-level dependency on high-level design decisions.
return `tmp-${Date.now()}-${tempFileNameCounter++}`;
}