Skip to content

Commit

Permalink
feat: Add listRetain and allow slices for listFetch (#322)
Browse files Browse the repository at this point in the history
* Add listRetain and allow slices for listFetch

* Correct documentation

* faster tracing and better validation

---------

Co-authored-by: Ben Kugler <[email protected]>
  • Loading branch information
ben-kugler and ben-kugler authored Mar 14, 2023
1 parent e1c2b91 commit b58d74d
Show file tree
Hide file tree
Showing 7 changed files with 323 additions and 7 deletions.
49 changes: 47 additions & 2 deletions src/cache-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
CacheListPushBack,
CacheListPushFront,
CacheListRemoveValue,
CacheListRetain,
CacheSet,
CacheDictionaryFetch,
CacheDictionarySetField,
Expand Down Expand Up @@ -58,6 +59,8 @@ import {
SortedSetFetchByRankCallOptions,
SortedSetFetchByScoreCallOptions,
SortedSetOrder,
ListRetainCallOptions,
ListFetchCallOptions,
} from './utils/cache-call-options';

// Type aliases to differentiate the different methods' optional arguments.
Expand Down Expand Up @@ -256,17 +259,26 @@ export class CacheClient {
*
* @param {string} cacheName - The cache containing the list.
* @param {string} listName - The list to fetch.
* @param {ListFetchCallOptions} [options]
* @param {number} [options.startIndex] - Start inclusive index for fetch operation.
* @param {number} [options.endIndex] - End exclusive index for fetch operation.
* @returns {Promise<CacheListFetch.Response>} -
* {@link CacheListFetch.Hit} containing the list elements if the list exists.
* {@link CacheListFetch.Miss} if the list does not exist.
* {@link CacheListFetch.Error} on failure.
*/
public async listFetch(
cacheName: string,
listName: string
listName: string,
options?: ListFetchCallOptions
): Promise<CacheListFetch.Response> {
const client = this.getNextDataClient();
return await client.listFetch(cacheName, listName);
return await client.listFetch(
cacheName,
listName,
options?.startIndex,
options?.endIndex
);
}

/**
Expand Down Expand Up @@ -412,6 +424,39 @@ export class CacheClient {
return await client.listRemoveValue(cacheName, listName, value);
}

/**
* Retains slice of elements of a given list, deletes the rest of the list
* that isn't being retained. Returns a Success or Error.
*
* @param {string} cacheName - The cache containing the list.
* @param {string} listName - The list to retain a slice of.
* @param {ListRetainCallOptions} [options]
* @param {number} [options.startIndex] - Start inclusive index for fetch
* operation. Defaults to start of array if not given, 0.
* @param {number} [options.endIndex] - End exclusive index for fetch
* operation. Defaults to end of array if not given.
* @param {CollectionTtl} [options.ttl] - How the TTL should be managed.
* Refreshes the list's TTL using the client's default if this is not
* supplied.
* @returns {Promise<CacheListRetain.Response>} -
* {@link CacheListRetain.Success} on success.
* {@link CacheListRetain.Error} on failure.
*/
public async listRetain(
cacheName: string,
listName: string,
options?: ListRetainCallOptions
): Promise<CacheListRetain.Response> {
const client = this.getNextDataClient();
return await client.listRetain(
cacheName,
listName,
options?.startIndex,
options?.endIndex,
options?.ttl
);
}

/**
* Fetches all elements of the given set
*
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as CacheListPopFront from './messages/responses/cache-list-pop-front';
import * as CacheListPushBack from './messages/responses/cache-list-push-back';
import * as CacheListPushFront from './messages/responses/cache-list-push-front';
import * as CacheListRemoveValue from './messages/responses/cache-list-remove-value';
import * as CacheListRetain from './messages/responses/cache-list-retain';
import * as CacheSet from './messages/responses/cache-set';
import * as CacheDelete from './messages/responses/cache-delete';
import * as CacheFlush from './messages/responses/cache-flush';
Expand Down Expand Up @@ -107,6 +108,7 @@ export {
CacheListPushBack,
CacheListPushFront,
CacheListRemoveValue,
CacheListRetain,
CacheSet,
CacheSetIfNotExists,
CacheDelete,
Expand Down
115 changes: 110 additions & 5 deletions src/internal/data-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
CacheSortedSetRemoveElement,
CacheSortedSetRemoveElements,
CacheSortedSetPutElements,
CacheListRetain,
} from '..';
import {version} from '../../package.json';
import {IdleGrpcClientWrapper} from './grpc/idle-grpc-client-wrapper';
Expand All @@ -65,12 +66,15 @@ import {
validateSortedSetCount,
validateSortedSetRanks,
validateSortedSetScores,
validateListSliceStartEnd,
} from './utils/validators';
import {CacheClientProps} from '../cache-client-props';
import {Middleware} from '../config/middleware/middleware';
import {middlewaresInterceptor} from './grpc/middlewares-interceptor';
import {truncateString} from './utils/display';
import {SdkError} from '../errors/errors';
import {cache_client} from '@gomomento/generated-types/dist/cacheclient';
import _Unbounded = cache_client._Unbounded;

export class DataClient {
private readonly clientWrapper: GrpcClientWrapper<grpcCache.ScsClient>;
Expand Down Expand Up @@ -683,27 +687,52 @@ export class DataClient {

public async listFetch(
cacheName: string,
listName: string
listName: string,
startIndex?: number,
endIndex?: number
): Promise<CacheListFetch.Response> {
try {
validateCacheName(cacheName);
validateListName(listName);
validateListSliceStartEnd(startIndex, endIndex);
} catch (err) {
return new CacheListFetch.Error(normalizeSdkError(err as Error));
}
this.logger.trace(`Issuing 'listFetch' request; listName: ${listName}`);
const result = await this.sendListFetch(cacheName, this.convert(listName));
this.logger.trace(`'listFetch' request result: ${result.toString()}`);
this.logger.trace(
"Issuing 'listFetch' request; listName: %s, startIndex: %s, endIndex: %s",
listName,
startIndex ?? 'null',
endIndex ?? 'null'
);
const result = await this.sendListFetch(
cacheName,
this.convert(listName),
startIndex,
endIndex
);
this.logger.trace("'listFetch' request result: %s", result.toString());
return result;
}

private async sendListFetch(
cacheName: string,
listName: Uint8Array
listName: Uint8Array,
start?: number,
end?: number
): Promise<CacheListFetch.Response> {
const request = new grpcCache._ListFetchRequest({
list_name: listName,
});
if (start) {
request.inclusive_start = start;
} else {
request.unbounded_start = new _Unbounded();
}
if (end) {
request.exclusive_end = end;
} else {
request.unbounded_end = new _Unbounded();
}
const metadata = this.createMetadata(cacheName);

return await new Promise(resolve => {
Expand All @@ -726,6 +755,82 @@ export class DataClient {
});
}

public async listRetain(
cacheName: string,
listName: string,
startIndex?: number,
endIndex?: number,
ttl: CollectionTtl = CollectionTtl.fromCacheTtl()
): Promise<CacheListRetain.Response> {
try {
validateCacheName(cacheName);
validateListName(listName);
validateListSliceStartEnd(startIndex, endIndex);
} catch (err) {
return new CacheListRetain.Error(normalizeSdkError(err as Error));
}
this.logger.trace(
"Issuing 'listRetain' request; listName: %s, startIndex: %s, endIndex: %s, ttl: %s",
listName,
startIndex ?? 'null',
endIndex ?? 'null',
ttl.ttlSeconds.toString() ?? 'null'
);
const result = await this.sendListRetain(
cacheName,
this.convert(listName),
startIndex,
endIndex,
ttl.ttlMilliseconds() || this.defaultTtlSeconds * 1000,
ttl.refreshTtl()
);
this.logger.trace("'listRetain' request result: %s", result.toString());
return result;
}

private async sendListRetain(
cacheName: string,
listName: Uint8Array,
start?: number,
end?: number,
ttlMilliseconds?: number,
refreshTtl?: boolean
): Promise<CacheListRetain.Response> {
const request = new grpcCache._ListRetainRequest({
list_name: listName,
ttl_milliseconds: ttlMilliseconds,
refresh_ttl: refreshTtl,
});
if (start) {
request.inclusive_start = start;
} else {
request.unbounded_start = new _Unbounded();
}
if (end) {
request.exclusive_end = end;
} else {
request.unbounded_end = new _Unbounded();
}
const metadata = this.createMetadata(cacheName);

return await new Promise(resolve => {
this.clientWrapper.getClient().ListRetain(
request,
metadata,
{
interceptors: this.interceptors,
},
(err, resp) => {
if (resp) {
resolve(new CacheListRetain.Success());
} else {
resolve(new CacheListRetain.Error(cacheServiceErrorMapper(err)));
}
}
);
});
}

public async listLength(
cacheName: string,
listName: string
Expand Down
14 changes: 14 additions & 0 deletions src/internal/utils/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ export function validateListName(name: string) {
}
}

export function validateListSliceStartEnd(
startIndex?: number,
endIndex?: number
) {
if (startIndex === undefined || endIndex === undefined) return;
// can't validate bounds for start and index of either or are negative without list length
if (startIndex > 0 || endIndex < 0) return;
if (endIndex <= startIndex) {
throw new InvalidArgumentError(
'endIndex (exclusive) must be larger than startIndex (inclusive)'
);
}
}

export function validateTtlMinutes(ttlMinutes: number) {
if (ttlMinutes < 0) {
throw new InvalidArgumentError('ttlMinutes must be positive');
Expand Down
48 changes: 48 additions & 0 deletions src/messages/responses/cache-list-retain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {SdkError} from '../../errors/errors';
import {ResponseBase, ResponseError, ResponseSuccess} from './response-base';

/**
* Parent response type for a list retain request. The
* response object is resolved to a type-safe object of one of
* the following subtypes:
*
* - {Success}
* - {Error}
*
* `instanceof` type guards can be used to operate on the appropriate subtype.
* @example
* For example:
* ```
* if (response instanceof CacheListRetain.Error) {
* // Handle error as appropriate. The compiler will smart-cast `response` to type
* // `CacheListRetain.Error` in this block, so you will have access to the properties
* // of the Error class; e.g. `response.errorCode()`.
* }
* ```
*/
export abstract class Response extends ResponseBase {}

class _Success extends Response {}

/**
* Indicates a Successful list retain request.
*/
export class Success extends ResponseSuccess(_Success) {}

class _Error extends Response {
constructor(protected _innerException: SdkError) {
super();
}
}

/**
* Indicates that an error occurred during the list retain request.
*
* This response object includes the following fields that you can use to determine
* how you would like to handle the error:
*
* - `errorCode()` - a unique Momento error code indicating the type of error that occurred.
* - `message()` - a human-readable description of the error
* - `innerException()` - the original error that caused the failure; can be re-thrown.
*/
export class Error extends ResponseError(_Error) {}
23 changes: 23 additions & 0 deletions src/utils/cache-call-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,26 @@ export interface SortedSetFetchByScoreCallOptions {
*/
count?: number;
}

export interface ListRetainCallOptions extends CollectionCallOptions {
/**
* Starting inclusive index of operation.
*/
startIndex?: number;

/**
* Ending exclusive index of operation.
*/
endIndex?: number;
}
export interface ListFetchCallOptions {
/**
* Starting inclusive index of operation.
*/
startIndex?: number;

/**
* Ending exclusive index of operation.
*/
endIndex?: number;
}
Loading

0 comments on commit b58d74d

Please sign in to comment.