tsoa
This commit is contained in:
114
node_modules/@hapi/catbox/lib/client.js
generated
vendored
Executable file
114
node_modules/@hapi/catbox/lib/client.js
generated
vendored
Executable file
@@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
|
||||
const Hoek = require('@hapi/hoek');
|
||||
const Boom = require('@hapi/boom');
|
||||
|
||||
|
||||
const internals = {
|
||||
validate: Symbol('validate')
|
||||
};
|
||||
|
||||
|
||||
internals.defaults = {
|
||||
partition: 'catbox'
|
||||
};
|
||||
|
||||
|
||||
module.exports = class {
|
||||
|
||||
constructor(engine, options) {
|
||||
|
||||
Hoek.assert(engine, 'Missing catbox client engine');
|
||||
Hoek.assert((typeof engine === 'object' && typeof engine.start === 'function') || typeof engine === 'function', 'engine must be an engine object or engine prototype (function)');
|
||||
Hoek.assert(typeof engine === 'function' || !options, 'Can only specify options with function engine config');
|
||||
|
||||
const settings = Object.assign({}, internals.defaults, options);
|
||||
Hoek.assert(settings.partition.match(/^[\w\-]+$/), 'Invalid partition name:' + settings.partition);
|
||||
|
||||
this.connection = (typeof engine === 'object' ? engine : new engine(settings));
|
||||
}
|
||||
|
||||
async start() {
|
||||
|
||||
await this.connection.start();
|
||||
}
|
||||
|
||||
async stop() {
|
||||
|
||||
await this.connection.stop();
|
||||
}
|
||||
|
||||
isReady() {
|
||||
|
||||
return this.connection.isReady();
|
||||
}
|
||||
|
||||
validateSegmentName(name) {
|
||||
|
||||
return this.connection.validateSegmentName(name);
|
||||
}
|
||||
|
||||
async get(key) {
|
||||
|
||||
this[internals.validate](key, null);
|
||||
|
||||
if (key === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await this.connection.get(key);
|
||||
if (!result ||
|
||||
result.item === undefined ||
|
||||
result.item === null) {
|
||||
|
||||
return null; // Not found
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const expires = result.stored + result.ttl;
|
||||
const ttl = expires - now;
|
||||
if (ttl <= 0) {
|
||||
return null; // Expired
|
||||
}
|
||||
|
||||
const cached = {
|
||||
item: result.item,
|
||||
stored: result.stored,
|
||||
ttl
|
||||
};
|
||||
|
||||
return cached; // Valid
|
||||
}
|
||||
|
||||
async set(key, value, ttl) {
|
||||
|
||||
this[internals.validate](key);
|
||||
|
||||
if (ttl <= 0) {
|
||||
return; // Not cachable (or bad rules)
|
||||
}
|
||||
|
||||
await this.connection.set(key, value, ttl);
|
||||
}
|
||||
|
||||
async drop(key) {
|
||||
|
||||
this[internals.validate](key);
|
||||
|
||||
await this.connection.drop(key); // Always drop, regardless of caching rules
|
||||
}
|
||||
|
||||
[internals.validate](key, allow = {}) {
|
||||
|
||||
if (!this.isReady()) {
|
||||
throw Boom.internal('Disconnected'); // Disconnected
|
||||
}
|
||||
|
||||
const isValidKey = (key && typeof key.id === 'string' &&
|
||||
key.segment && typeof key.segment === 'string');
|
||||
|
||||
if (!isValidKey && key !== allow) {
|
||||
throw Boom.internal('Invalid key');
|
||||
}
|
||||
}
|
||||
};
|
||||
299
node_modules/@hapi/catbox/lib/index.d.ts
generated
vendored
Normal file
299
node_modules/@hapi/catbox/lib/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,299 @@
|
||||
// TypeScript Version: 4.9
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import { Podium } from "@hapi/podium";
|
||||
|
||||
/**
|
||||
* Client
|
||||
* The Client object provides a low-level cache abstraction. The object is constructed using new Client(engine, options) where:
|
||||
* engine - is an object or a prototype function implementing the cache strategy:
|
||||
* * function - a prototype function with the signature function(options). catbox will call new func(options).
|
||||
* * object - a pre instantiated client implementation object. Does not support passing options.
|
||||
* options - the strategy configuration object. Each strategy defines its own configuration options with the following common options:
|
||||
* * partition - the partition name used to isolate the cached results across multiple clients. The partition name is used as the MongoDB database name,
|
||||
* the Riak bucket, or as a key prefix in Redis and Memcached. To share the cache across multiple clients, use the same partition name.
|
||||
* @see {@link https://github.com/hapijs/catbox#client}
|
||||
*/
|
||||
export class Client<T> implements ClientApi<T> {
|
||||
constructor(engine: ClientApi<any>);
|
||||
constructor(engine: EnginePrototype<any>, options?: ClientOptions);
|
||||
|
||||
/** start() - creates a connection to the cache server. Must be called before any other method is available. */
|
||||
start(): Promise<void>;
|
||||
/** stop() - terminates the connection to the cache server. */
|
||||
stop(): Promise<void>;
|
||||
/**
|
||||
* get(key, callback) - retrieve an item from the cache engine if found where:
|
||||
* * key - a cache key object (see [ICacheKey]).
|
||||
*/
|
||||
get(key: CacheKey): Promise<null | CachedObject<T>>;
|
||||
/**
|
||||
* set(key, value, ttl, callback) - store an item in the cache for a specified length of time, where:
|
||||
* * key - a cache key object (see [ICacheKey]).
|
||||
* * value - the string or object value to be stored.
|
||||
* * ttl - a time-to-live value in milliseconds after which the item is automatically removed from the cache (or is marked invalid).
|
||||
*/
|
||||
set(key: CacheKey, value: T, ttl: number): Promise<void>;
|
||||
/**
|
||||
* drop(key, callback) - remove an item from cache where:
|
||||
* * key - a cache key object (see [ICacheKey]).
|
||||
*/
|
||||
drop(key: CacheKey): Promise<void>;
|
||||
/** isReady() - returns true if cache engine determines itself as ready, false if it is not ready. */
|
||||
isReady(): boolean;
|
||||
/** validateSegmentName(segment) - returns null if the segment name is valid (see below), otherwise should return an instance of Error with an appropriate message. */
|
||||
validateSegmentName(segment: string): null | Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* A prototype CatBox engine function
|
||||
*/
|
||||
export interface EnginePrototype<T> {
|
||||
new(settings: ClientOptions): ClientApi<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client API
|
||||
* The Client object provides the following methods:
|
||||
* @see {@link https://github.com/hapijs/catbox#api}
|
||||
*/
|
||||
export interface ClientApi<T> {
|
||||
/** start() - creates a connection to the cache server. Must be called before any other method is available. */
|
||||
start(): Promise<void>;
|
||||
/** stop() - terminates the connection to the cache server. */
|
||||
stop(): void;
|
||||
/**
|
||||
* get(key, callback) - retrieve an item from the cache engine if found where:
|
||||
* * key - a cache key object (see [ICacheKey]).
|
||||
*/
|
||||
get(key: CacheKey): Promise<null | CachedObject<T>>;
|
||||
/**
|
||||
* set(key, value, ttl) - store an item in the cache for a specified length of time, where:
|
||||
* * key - a cache key object (see [ICacheKey]).
|
||||
* * value - the string or object value to be stored.
|
||||
* * ttl - a time-to-live value in milliseconds after which the item is automatically removed from the cache (or is marked invalid).
|
||||
*/
|
||||
set(key: CacheKey, value: T, ttl: number): Promise<void>;
|
||||
/**
|
||||
* drop(key) - remove an item from cache where:
|
||||
* * key - a cache key object (see [ICacheKey]).
|
||||
*/
|
||||
drop(key: CacheKey): Promise<void>;
|
||||
/** isReady() - returns true if cache engine determines itself as ready, false if it is not ready. */
|
||||
isReady(): boolean;
|
||||
/** validateSegmentName(segment) - returns null if the segment name is valid (see below), otherwise should return an instance of Error with an appropriate message. */
|
||||
validateSegmentName(segment: string): null | Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Any method with a key argument takes an object with the following required properties:
|
||||
*/
|
||||
export interface CacheKey {
|
||||
/** segment - a caching segment name string. Enables using a single cache server for storing different sets of items with overlapping ids. */
|
||||
segment: string;
|
||||
/** id - a unique item identifier string (per segment). Can be an empty string. */
|
||||
id: string;
|
||||
}
|
||||
|
||||
/** Cached object contains the following: */
|
||||
export interface CachedObject<T> {
|
||||
/** item - the value stored in the cache using set(). */
|
||||
item: T;
|
||||
/** stored - the timestamp when the item was stored in the cache (in milliseconds). */
|
||||
stored: number;
|
||||
/** ttl - the remaining time-to-live (not the original value used when storing the object). */
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
/**
|
||||
* this will store items under keys that start with this value.
|
||||
*/
|
||||
partition?: string | undefined;
|
||||
}
|
||||
|
||||
export type PolicyOptionVariants<T> = PolicyOptions<T> | DecoratedPolicyOptions<T>;
|
||||
|
||||
export type Id = string | { id: string };
|
||||
|
||||
/**
|
||||
* The Policy object provides a convenient cache interface by setting a
|
||||
* global policy which is automatically applied to every storage action.
|
||||
* The object is constructed using new Policy(options, [cache, segment]) where:
|
||||
* * options - an object with the IPolicyOptions structure
|
||||
* * cache - a Client instance (which has already been started).
|
||||
* * segment - required when cache is provided. The segment name used to
|
||||
* isolate cached items within the cache partition.
|
||||
* @see {@link https://github.com/hapijs/catbox#policy}
|
||||
*/
|
||||
export class Policy<T, O extends PolicyOptionVariants<T>> {
|
||||
constructor(options: O, cache: Client<T>, segment: string);
|
||||
/**
|
||||
* retrieve an item from the cache. If the item is not
|
||||
* found and the generateFunc method was provided,
|
||||
* a new value is generated, stored in the cache, and returned.
|
||||
* Multiple concurrent requests are queued and processed once. The method arguments are:
|
||||
* @param id the unique item identifier (within the policy segment).
|
||||
* Can be a string or an object with the required 'id' key.
|
||||
*/
|
||||
get(id: Id): Promise<O extends DecoratedPolicyOptions<T> ? DecoratedResult<T> : T | null>;
|
||||
|
||||
/**
|
||||
* store an item in the cache where:
|
||||
* @param id - the unique item identifier (within the policy segment).
|
||||
* @param value - the string or object value to be stored.
|
||||
* @param ttl - a time-to-live override value in milliseconds after which the item is automatically
|
||||
* removed from the cache (or is marked invalid).
|
||||
* This should be set to 0 in order to use the caching rules configured when creating the Policy object.
|
||||
*/
|
||||
set(id: Id, value: T, ttl?: number): Promise<void>;
|
||||
/**
|
||||
* remove the item from cache where:
|
||||
* @param id the unique item identifier (within the policy segment).
|
||||
*/
|
||||
drop(id: Id): Promise<void>;
|
||||
/**
|
||||
* given a created timestamp in milliseconds, returns the time-to-live left
|
||||
* based on the configured rules.
|
||||
*/
|
||||
ttl(created: number): number;
|
||||
/** changes the policy rules after construction (note that items already stored will not be affected) */
|
||||
rules(options: PolicyOptions<T>): void;
|
||||
/**
|
||||
* returns true if cache engine determines itself as ready, false if it is not ready or if
|
||||
* here is no cache engine set.
|
||||
*/
|
||||
isReady(): boolean;
|
||||
/** an object with cache statistics */
|
||||
stats: CacheStatisticsObject;
|
||||
/**
|
||||
* a podium event emitter, emitting 'error' events under the 'persist' (emits any cache errors
|
||||
* thrown during the generation of new values as a result of a get() request) and 'generate'
|
||||
* (emits any new value generation errors thrown as a result of a get() request) channels.
|
||||
*/
|
||||
events: Podium;
|
||||
/**
|
||||
* a reference to the cache client if set.
|
||||
*/
|
||||
client: Client<T>;
|
||||
}
|
||||
|
||||
export interface DecoratedResult<T> {
|
||||
value: T;
|
||||
cached: PolicyGetCachedOptions<T>;
|
||||
report: PolicyGetReportLog;
|
||||
}
|
||||
|
||||
export interface PolicyGetCachedOptions<T> {
|
||||
/** item - the cached value. */
|
||||
item: T;
|
||||
/** stored - the timestamp when the item was stored in the cache. */
|
||||
stored: number;
|
||||
/** ttl - the cache ttl value for the record. */
|
||||
ttl: number;
|
||||
/** isStale - true if the item is stale. */
|
||||
isStale: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see {@link https://github.com/hapijs/catbox#policy}
|
||||
*/
|
||||
export interface PolicyOptions<T> {
|
||||
/** expiresIn - relative expiration expressed in the number of milliseconds since the item was saved in the cache. Cannot be used together with expiresAt. */
|
||||
expiresIn?: number | undefined;
|
||||
/** expiresAt - time of day expressed in 24h notation using the 'HH:MM' format, at which point all cache records for the route expire. Uses local time. Cannot be used together with expiresIn. */
|
||||
expiresAt?: string | undefined;
|
||||
/** generateFunc - a function used to generate a new cache item if one is not found in the cache when calling get(). The method's signature is function(id, next) where: */
|
||||
generateFunc?: GenerateFunc<T> | undefined;
|
||||
/**
|
||||
* staleIn - number of milliseconds to mark an item stored in cache as stale and attempt to regenerate it when generateFunc is provided.
|
||||
* Must be less than expiresIn. Alternatively function that returns staleIn value in milliseconds. The function signature is function(stored, ttl) where:
|
||||
* * stored - the timestamp when the item was stored in the cache (in milliseconds).
|
||||
* * ttl - the remaining time-to-live (not the original value used when storing the object).
|
||||
*/
|
||||
staleIn?: number | ((stored: number, ttl: number) => number) | undefined;
|
||||
/** staleTimeout - number of milliseconds to wait before returning a stale value while generateFunc is generating a fresh value. */
|
||||
staleTimeout?: number | undefined;
|
||||
/**
|
||||
* generateTimeout - number of milliseconds to wait before returning a timeout error when the generateFunc function takes too long to return a value.
|
||||
* When the value is eventually returned, it is stored in the cache for future requests. Required if generateFunc is present.
|
||||
* Set to false to disable timeouts which may cause all get() requests to get stuck forever.
|
||||
*/
|
||||
generateTimeout?: number | false | undefined;
|
||||
/** dropOnError - if true, an error or timeout in the generateFunc causes the stale value to be evicted from the cache. Defaults to true. */
|
||||
dropOnError?: boolean | undefined;
|
||||
/** generateOnReadError - if false, an upstream cache read error will stop the get() method from calling the generate function and will instead pass back the cache error. Defaults to true. */
|
||||
generateOnReadError?: boolean | undefined;
|
||||
/** generateIgnoreWriteError - if false, an upstream cache write error will be passed back with the generated value when calling the get() method. Defaults to true. */
|
||||
generateIgnoreWriteError?: boolean | undefined;
|
||||
/**
|
||||
* pendingGenerateTimeout - number of milliseconds while generateFunc call is in progress for a given id, before a subsequent generateFunc call is allowed.
|
||||
* @default 0, no blocking of concurrent generateFunc calls beyond staleTimeout.
|
||||
*/
|
||||
pendingGenerateTimeout?: number | undefined;
|
||||
}
|
||||
|
||||
export interface DecoratedPolicyOptions<T> extends PolicyOptions<T> {
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
getDecoratedValue: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface GenerateFuncFlags {
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* generateFunc
|
||||
* Is used in PolicyOptions
|
||||
* A function used to generate a new cache item if one is not found in the cache when calling get(). The method's signature is function(id)
|
||||
* @param id - the id string or object provided to the get() method.
|
||||
* @param next - the method called when the new item is returned with the signature function(err, value, ttl) where:
|
||||
* * err - an error condition.
|
||||
* * value - the new value generated.
|
||||
* * ttl - the cache ttl value in milliseconds. Set to 0 to skip storing in the cache. Defaults to the cache global policy.
|
||||
* @see {@link https://github.com/hapijs/catbox#policy}
|
||||
*/
|
||||
export type GenerateFunc<T> = (id: Id, flags: GenerateFuncFlags) => Promise<T>;
|
||||
|
||||
/**
|
||||
* An object with logging information about the generation operation containing the following keys (as relevant):
|
||||
*/
|
||||
export interface PolicyGetReportLog {
|
||||
/** msec - the cache lookup time in milliseconds. */
|
||||
msec: number;
|
||||
/** stored - the timestamp when the item was stored in the cache. */
|
||||
stored: number;
|
||||
/** isStale - true if the item is stale. */
|
||||
isStale: boolean;
|
||||
/** ttl - the cache ttl value for the record. */
|
||||
ttl: number;
|
||||
/** error - lookup error. */
|
||||
error?: Error | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* an object with cache statistics where:
|
||||
*/
|
||||
export interface CacheStatisticsObject {
|
||||
/** sets - number of cache writes. */
|
||||
sets: number;
|
||||
/** gets - number of cache get() requests. */
|
||||
gets: number;
|
||||
/** hits - number of cache get() requests in which the requested id was found in the cache (can be stale). */
|
||||
hits: number;
|
||||
/** stales - number of cache reads with stale requests (only counts the first request in a queued get() operation). */
|
||||
stales: number;
|
||||
/** generates - number of calls to the generate function. */
|
||||
generates: number;
|
||||
/** errors - cache operations errors. TODO check this */
|
||||
errors: number;
|
||||
}
|
||||
|
||||
// Definitions adapted from DefinitelyTyped, originally created by:
|
||||
// Jason Swearingen <https://github.com/jasonswearingen>
|
||||
// AJP <https://github.com/AJamesPhillips>
|
||||
// Rodrigo Saboya <https://github.com/saboya>
|
||||
// Silas Rech <https://github.com/lenovouser>
|
||||
13
node_modules/@hapi/catbox/lib/index.js
generated
vendored
Executable file
13
node_modules/@hapi/catbox/lib/index.js
generated
vendored
Executable file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
const Client = require('./client');
|
||||
const Policy = require('./policy');
|
||||
|
||||
|
||||
const internals = {};
|
||||
|
||||
|
||||
exports.Client = Client;
|
||||
|
||||
|
||||
exports.Policy = exports.policy = Policy;
|
||||
61
node_modules/@hapi/catbox/lib/pending.js
generated
vendored
Executable file
61
node_modules/@hapi/catbox/lib/pending.js
generated
vendored
Executable file
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
|
||||
const internals = {};
|
||||
|
||||
|
||||
exports = module.exports = class {
|
||||
|
||||
id = null;
|
||||
timeout = null;
|
||||
count = 1;
|
||||
rule = null;
|
||||
resolve = null;
|
||||
reject = null;
|
||||
|
||||
constructor(id, rule) {
|
||||
|
||||
this.id = id;
|
||||
this.rule = rule;
|
||||
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
}
|
||||
|
||||
join() {
|
||||
|
||||
++this.count;
|
||||
return this.promise;
|
||||
}
|
||||
|
||||
send(err, value, cached, report) {
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
|
||||
if (err &&
|
||||
!cached) {
|
||||
|
||||
this.reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.rule.getDecoratedValue) {
|
||||
this.resolve(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
report.error = err;
|
||||
}
|
||||
|
||||
this.resolve({ value, cached, report });
|
||||
}
|
||||
|
||||
setTimeout(fn, timeoutMs) {
|
||||
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = setTimeout(fn, timeoutMs);
|
||||
}
|
||||
};
|
||||
478
node_modules/@hapi/catbox/lib/policy.js
generated
vendored
Executable file
478
node_modules/@hapi/catbox/lib/policy.js
generated
vendored
Executable file
@@ -0,0 +1,478 @@
|
||||
'use strict';
|
||||
|
||||
const Boom = require('@hapi/boom');
|
||||
const Hoek = require('@hapi/hoek');
|
||||
const Podium = require('@hapi/podium');
|
||||
const Validate = require('@hapi/validate');
|
||||
|
||||
const Pending = require('./pending');
|
||||
|
||||
|
||||
const internals = {
|
||||
day: 24 * 60 * 60 * 1000,
|
||||
events: Podium.validate([
|
||||
{ name: 'error', channels: ['generate', 'persist'] }
|
||||
])
|
||||
};
|
||||
|
||||
|
||||
internals.schema = Validate.object({
|
||||
expiresIn: Validate.number().integer().min(1),
|
||||
expiresAt: Validate.string().regex(/^\d\d?\:\d\d$/),
|
||||
staleIn: [
|
||||
Validate.number().integer().min(1).when('expiresAt', { is: Validate.required(), then: Validate.number().max(86400000 - 1) }), // One day - 1 (max is inclusive)
|
||||
Validate.func()
|
||||
],
|
||||
staleTimeout: Validate.number().integer().min(1),
|
||||
generateFunc: Validate.func(),
|
||||
generateTimeout: Validate.number().integer().min(1).allow(false),
|
||||
generateOnReadError: Validate.boolean(),
|
||||
generateIgnoreWriteError: Validate.boolean(),
|
||||
dropOnError: Validate.boolean(),
|
||||
pendingGenerateTimeout: Validate.number().integer().min(1),
|
||||
getDecoratedValue: Validate.boolean().default(false),
|
||||
|
||||
// Ignored external keys (hapi)
|
||||
|
||||
privacy: Validate.any(),
|
||||
cache: Validate.any(),
|
||||
segment: Validate.any(),
|
||||
shared: Validate.any()
|
||||
})
|
||||
.without('expiresIn', 'expiresAt')
|
||||
.with('staleIn', 'generateFunc')
|
||||
.with('generateOnReadError', 'generateFunc')
|
||||
.with('generateIgnoreWriteError', 'generateFunc')
|
||||
.with('dropOnError', 'generateFunc')
|
||||
.and('generateFunc', 'generateTimeout')
|
||||
.and('staleIn', 'staleTimeout');
|
||||
|
||||
|
||||
exports = module.exports = internals.Policy = class {
|
||||
|
||||
rule = null;
|
||||
stats = {
|
||||
sets: 0,
|
||||
gets: 0,
|
||||
hits: 0,
|
||||
stales: 0,
|
||||
generates: 0,
|
||||
errors: 0
|
||||
};
|
||||
|
||||
_events = null;
|
||||
_cache = null;
|
||||
_segment = null;
|
||||
_pendings = new Map(); // id -> Pending
|
||||
_pendingGenerateCall = new Map(); // id -> timer
|
||||
|
||||
constructor(options, cache, segment) {
|
||||
|
||||
this._cache = cache;
|
||||
this.rules(options);
|
||||
|
||||
if (cache) {
|
||||
const nameErr = cache.validateSegmentName(segment);
|
||||
Hoek.assert(nameErr === null, 'Invalid segment name: ' + segment + (nameErr ? ' (' + nameErr.message + ')' : ''));
|
||||
|
||||
this._segment = segment;
|
||||
}
|
||||
}
|
||||
|
||||
get client() {
|
||||
|
||||
return this._cache;
|
||||
}
|
||||
|
||||
get events() {
|
||||
|
||||
if (!this._events) {
|
||||
this._events = new Podium.Podium(internals.events, { validate: false });
|
||||
}
|
||||
|
||||
return this._events;
|
||||
}
|
||||
|
||||
_error(source, error) {
|
||||
|
||||
if (!this._events) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._events.emit({ name: 'error', channel: source }, { source, error });
|
||||
}
|
||||
|
||||
rules(options) {
|
||||
|
||||
this.rule = internals.Policy.compile(options, !!this._cache);
|
||||
}
|
||||
|
||||
async get(key) { // key: string or { id: 'id' }
|
||||
|
||||
++this.stats.gets;
|
||||
|
||||
// Check if request is already pending
|
||||
|
||||
if (!key ||
|
||||
typeof key === 'string') {
|
||||
|
||||
key = { id: key, string: true };
|
||||
}
|
||||
|
||||
let pending = this._pendings.get(key.id);
|
||||
if (pending !== undefined) {
|
||||
return pending.join();
|
||||
}
|
||||
|
||||
pending = new Pending(key.id, this.rule);
|
||||
this._pendings.set(key.id, pending);
|
||||
|
||||
try {
|
||||
await this._get(pending, key);
|
||||
}
|
||||
catch (err) {
|
||||
this._send(key, err); // Safeguard to ensure that the pending rejects on any processing errors
|
||||
}
|
||||
|
||||
return pending.promise;
|
||||
}
|
||||
|
||||
async _get(pending, key) {
|
||||
|
||||
// Prepare report
|
||||
|
||||
const report = {};
|
||||
|
||||
// Lookup in cache
|
||||
|
||||
const timer = new Hoek.Bench();
|
||||
|
||||
if (this._cache) {
|
||||
try {
|
||||
var cached = await this._cache.get({ segment: this._segment, id: key.id });
|
||||
}
|
||||
catch (err) {
|
||||
report.error = err;
|
||||
++this.stats.errors;
|
||||
this._error('persist', err);
|
||||
}
|
||||
}
|
||||
|
||||
report.msec = timer.elapsed();
|
||||
|
||||
if (cached) {
|
||||
report.stored = cached.stored;
|
||||
report.ttl = cached.ttl;
|
||||
const staleIn = typeof this.rule.staleIn === 'function' ? this.rule.staleIn(cached.stored, cached.ttl) : this.rule.staleIn;
|
||||
cached.isStale = staleIn ? Date.now() - cached.stored >= staleIn : false;
|
||||
report.isStale = cached.isStale;
|
||||
|
||||
if (cached.isStale) {
|
||||
++this.stats.stales;
|
||||
}
|
||||
}
|
||||
|
||||
// No generate method
|
||||
|
||||
if (!this.rule.generateFunc ||
|
||||
report.error && !this.rule.generateOnReadError) {
|
||||
|
||||
this._send(key, report.error, cached ? cached.item : null, cached, report);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if found and fresh
|
||||
|
||||
if (cached &&
|
||||
!cached.isStale) {
|
||||
|
||||
this._send(key, null, cached.item, cached, report);
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until generated or otherwise resolved
|
||||
|
||||
return Promise.race([
|
||||
pending.promise,
|
||||
this._generate(pending, key, cached, report)
|
||||
]);
|
||||
}
|
||||
|
||||
_generate(pending, key, cached, report) {
|
||||
|
||||
if (cached) { // Must be stale
|
||||
|
||||
// Set stale timeout
|
||||
|
||||
cached.ttl = cached.ttl - this.rule.staleTimeout; // Adjust TTL for when the timeout is invoked (staleTimeout must be valid if isStale is true)
|
||||
}
|
||||
|
||||
if (cached &&
|
||||
cached.ttl > 0) {
|
||||
|
||||
pending.setTimeout(() => this._send(key, null, cached.item, cached, report), this.rule.staleTimeout);
|
||||
}
|
||||
else if (this.rule.generateTimeout) {
|
||||
|
||||
// Set item generation timeout (when not in cache)
|
||||
|
||||
pending.setTimeout(() => this._send(key, Boom.serverUnavailable(), null, null, report), this.rule.generateTimeout);
|
||||
}
|
||||
|
||||
// Check if a generate call is already in progress
|
||||
|
||||
if (this._pendingGenerateCall.has(key.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate new value
|
||||
|
||||
++this.stats.generates; // Record generation before call in case it times out
|
||||
|
||||
if (this.rule.pendingGenerateTimeout) {
|
||||
const timeout = setTimeout(() => this._pendingGenerateCall.delete(key.id), this.rule.pendingGenerateTimeout);
|
||||
this._pendingGenerateCall.set(key.id, timeout);
|
||||
}
|
||||
|
||||
return this._callGenerateFunc(key, cached, report);
|
||||
}
|
||||
|
||||
async _callGenerateFunc(key, cached, report) {
|
||||
|
||||
const flags = {};
|
||||
|
||||
try {
|
||||
var value = await this.rule.generateFunc(key.string ? key.id : key, flags);
|
||||
}
|
||||
catch (err) {
|
||||
var generateError = err;
|
||||
this._error('generate', err);
|
||||
}
|
||||
|
||||
const pendingTimeout = this._pendingGenerateCall.get(key.id);
|
||||
if (pendingTimeout) {
|
||||
clearTimeout(pendingTimeout);
|
||||
this._pendingGenerateCall.delete(key.id);
|
||||
}
|
||||
|
||||
// Error (if dropOnError is not set to false) or not cached
|
||||
|
||||
try {
|
||||
if (flags.ttl === 0 || // null or undefined means use policy
|
||||
generateError && this.rule.dropOnError) {
|
||||
|
||||
await this.drop(key.id); // Invalidate cache
|
||||
}
|
||||
else if (!generateError) {
|
||||
await this.set(key.id, value, flags.ttl); // Replace stale cache copy with late-coming fresh copy
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
var persistError = err;
|
||||
this._error('persist', err);
|
||||
}
|
||||
|
||||
const error = generateError || (this.rule.generateIgnoreWriteError ? null : persistError);
|
||||
if (cached &&
|
||||
error &&
|
||||
!this.rule.dropOnError) {
|
||||
|
||||
this._send(key, error, cached.item, cached, report);
|
||||
return;
|
||||
}
|
||||
|
||||
this._send(key, error, value, null, report); // Ignored if stale value already returned
|
||||
}
|
||||
|
||||
_send(key, err, value, cached, report) {
|
||||
|
||||
const pending = this._pendings.get(key.id);
|
||||
if (!pending) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pendings.delete(key.id);
|
||||
pending.send(err, value, cached, report);
|
||||
|
||||
if (report?.isStale !== undefined) {
|
||||
this.stats.hits = this.stats.hits + pending.count;
|
||||
}
|
||||
}
|
||||
|
||||
async set(key, value, ttl) {
|
||||
|
||||
++this.stats.sets;
|
||||
|
||||
if (!this._cache) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this._cache.set({ segment: this._segment, id: internals.id(key) }, value, ttl || internals.Policy.ttl(this.rule));
|
||||
}
|
||||
catch (err) {
|
||||
++this.stats.errors;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async drop(key) {
|
||||
|
||||
if (!this._cache) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this._cache.drop({ segment: this._segment, id: internals.id(key) });
|
||||
return;
|
||||
}
|
||||
catch (err) {
|
||||
++this.stats.errors;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
ttl(created) {
|
||||
|
||||
return internals.Policy.ttl(this.rule, created);
|
||||
}
|
||||
|
||||
isReady() {
|
||||
|
||||
if (!this._cache) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._cache.connection.isReady();
|
||||
}
|
||||
|
||||
static compile(options, serverSide) {
|
||||
|
||||
/*
|
||||
{
|
||||
expiresIn: 30000,
|
||||
expiresAt: '13:00',
|
||||
generateFunc: (id, flags) => { throw err; } / { return result; } / { flags.ttl = ttl; return result; }
|
||||
generateTimeout: 500,
|
||||
generateOnReadError: true,
|
||||
generateIgnoreWriteError: true,
|
||||
staleIn: 20000,
|
||||
staleTimeout: 500,
|
||||
dropOnError: true,
|
||||
getDecoratedValue: false
|
||||
}
|
||||
*/
|
||||
|
||||
const rule = {};
|
||||
|
||||
if (!options ||
|
||||
!Object.keys(options).length) {
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
// Validate rule
|
||||
|
||||
options = Validate.attempt(options, internals.schema, 'Invalid cache policy configuration');
|
||||
|
||||
const hasExpiresIn = options.expiresIn !== undefined && options.expiresIn !== null;
|
||||
const hasExpiresAt = options.expiresAt !== undefined && options.expiresAt !== null;
|
||||
|
||||
Hoek.assert(!hasExpiresIn || !options.staleIn || typeof options.staleIn === 'function' || options.staleIn < options.expiresIn, 'staleIn must be less than expiresIn');
|
||||
Hoek.assert(!options.staleIn || serverSide, 'Cannot use stale options without server-side caching');
|
||||
Hoek.assert(!options.staleTimeout || !hasExpiresIn || options.staleTimeout < options.expiresIn, 'staleTimeout must be less than expiresIn');
|
||||
Hoek.assert(!options.staleTimeout || !hasExpiresIn || typeof options.staleIn === 'function' || options.staleTimeout < options.expiresIn - options.staleIn, 'staleTimeout must be less than the delta between expiresIn and staleIn');
|
||||
Hoek.assert(!options.staleTimeout || !options.pendingGenerateTimeout || options.staleTimeout < options.pendingGenerateTimeout, 'pendingGenerateTimeout must be greater than staleTimeout if specified');
|
||||
|
||||
// Expiration
|
||||
|
||||
if (hasExpiresAt) {
|
||||
|
||||
// expiresAt
|
||||
|
||||
const time = /^(\d\d?):(\d\d)$/.exec(options.expiresAt);
|
||||
rule.expiresAt = {
|
||||
hours: parseInt(time[1], 10),
|
||||
minutes: parseInt(time[2], 10)
|
||||
};
|
||||
}
|
||||
else {
|
||||
|
||||
// expiresIn
|
||||
|
||||
rule.expiresIn = options.expiresIn ?? 0;
|
||||
}
|
||||
|
||||
// generateTimeout
|
||||
|
||||
if (options.generateFunc) {
|
||||
rule.generateFunc = options.generateFunc;
|
||||
rule.generateTimeout = options.generateTimeout;
|
||||
|
||||
// Stale
|
||||
|
||||
if (options.staleIn) {
|
||||
rule.staleIn = options.staleIn;
|
||||
rule.staleTimeout = options.staleTimeout;
|
||||
}
|
||||
|
||||
rule.dropOnError = options.dropOnError !== undefined ? options.dropOnError : true; // Defaults to true
|
||||
rule.pendingGenerateTimeout = options.pendingGenerateTimeout !== undefined ? options.pendingGenerateTimeout : 0; // Defaults to zero
|
||||
}
|
||||
|
||||
rule.generateOnReadError = options.generateOnReadError !== undefined ? options.generateOnReadError : true; // Defaults to true
|
||||
rule.generateIgnoreWriteError = options.generateIgnoreWriteError !== undefined ? options.generateIgnoreWriteError : true; // Defaults to true
|
||||
|
||||
// Decorations
|
||||
|
||||
rule.getDecoratedValue = options.getDecoratedValue;
|
||||
|
||||
return rule;
|
||||
}
|
||||
|
||||
static ttl(rule, created, now) {
|
||||
|
||||
now = now ?? Date.now();
|
||||
created = created ?? now;
|
||||
const age = now - created;
|
||||
|
||||
if (age < 0) {
|
||||
return 0; // Created in the future, assume expired/bad
|
||||
}
|
||||
|
||||
if (rule.expiresIn) {
|
||||
return Math.max(rule.expiresIn - age, 0);
|
||||
}
|
||||
|
||||
if (rule.expiresAt) {
|
||||
if (age > internals.day) { // If the item was created more than a 24 hours ago
|
||||
return 0;
|
||||
}
|
||||
|
||||
const expiresAt = new Date(created); // Compare expiration time on the same day
|
||||
expiresAt.setHours(rule.expiresAt.hours);
|
||||
expiresAt.setMinutes(rule.expiresAt.minutes);
|
||||
expiresAt.setSeconds(0);
|
||||
expiresAt.setMilliseconds(0);
|
||||
let expires = expiresAt.getTime();
|
||||
|
||||
if (expires <= created) {
|
||||
expires = expires + internals.day; // Move to tomorrow
|
||||
}
|
||||
|
||||
if (now >= expires) { // Expired
|
||||
return 0;
|
||||
}
|
||||
|
||||
return expires - now;
|
||||
}
|
||||
|
||||
return 0; // No rule
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
internals.id = function (key) {
|
||||
|
||||
return key && typeof key === 'object' ? key.id : key;
|
||||
};
|
||||
Reference in New Issue
Block a user