@hotmeshio/hotmesh
Version:
Serverless Workflow
321 lines (319 loc) • 11.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Search = void 0;
const key_1 = require("../../modules/key");
const storage_1 = require("../../modules/storage");
/**
* The Search module provides methods for reading and
* writing record data to a workflow. The instance
* methods exposed by this class are available
* for use from within a running workflow. The following example
* uses search to set a `name` field and increment a
* `counter` field. The workflow returns the incremented value.
*
* @example
* ```typescript
* //searchWorkflow.ts
* import { workflow } from '@hotmeshio/hotmesh';
* export async function searchExample(name: string): Promise<{counter: number}> {
* const search = await workflow.search();
* await search.set({ name });
* const newCounterValue = await search.incr('counter', 1);
* return { counter: newCounterValue };
* }
* ```
*/
class Search {
/**
* @private
*/
constructor(workflowId, hotMeshClient, searchSessionId) {
/**
* @private
*/
this.searchSessionIndex = 0;
/**
* @private
*/
this.cachedFields = {};
const keyParams = {
appId: hotMeshClient.appId,
jobId: workflowId,
};
this.jobId = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
this.searchSessionId = searchSessionId;
this.hotMeshClient = hotMeshClient;
this.search = hotMeshClient.engine.search;
}
/**
* Prefixes the key with an underscore to keep separate from the
* activity and job history (and searchable via HKEYS)
*
* @private
*/
safeKey(key) {
if (key.startsWith('"')) {
return key.slice(1, -1);
}
return `_${key}`;
}
/**
* For those deployments with search configured, this method
* will configure the search index with the provided schema.
*
* @private
* @example
* const search = {
* index: 'my_search_index',
* prefix: ['my_workflow_prefix'],
* schema: {
* field1: { type: 'TEXT', sortable: true },
* field2: { type: 'NUMERIC', sortable: true }
* }
* }
* await Search.configureSearchIndex(hotMeshClient, search);
*/
static async configureSearchIndex(hotMeshClient, search) {
if (search?.schema) {
const searchService = hotMeshClient.engine.search;
const schema = [];
for (const [key, value] of Object.entries(search.schema)) {
if (value.indexed !== false) {
schema.push(value.fieldName ? `${value.fieldName.toString()}` : `_${key}`);
schema.push(value.type ? value.type : 'TEXT');
if (value.noindex) {
schema.push('NOINDEX');
}
else {
if (value.nostem && value.type === 'TEXT') {
schema.push('NOSTEM');
}
if (value.sortable) {
schema.push('SORTABLE');
}
}
}
}
try {
const keyParams = {
appId: hotMeshClient.appId,
jobId: '',
};
const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
await searchService.createSearchIndex(`${search.index}`, prefixes, schema);
}
catch (error) {
hotMeshClient.engine.logger.info('meshflow-client-search-err', {
error,
});
}
}
}
/**
* Returns an array of search indexes ids
*
* @example
* const searchIndexes = await Search.listSearchIndexes(hotMeshClient);
*/
static async listSearchIndexes(hotMeshClient) {
try {
const searchService = hotMeshClient.engine.search;
return await searchService.listSearchIndexes();
}
catch (error) {
hotMeshClient.engine.logger.info('meshflow-client-search-list-err', {
error,
});
return [];
}
}
/**
* increments the index to return a unique search session guid when
* calling any method that produces side effects (changes the value)
* @private
*/
getSearchSessionGuid() {
return `${this.searchSessionId}-${this.searchSessionIndex++}-`;
}
/**
* Sets the fields listed in args. Returns the
* count of new fields that were set (does not
* count fields that were updated)
*
* @example
* const search = await workflow.search();
* const count = await search.set({ field1: 'value1', field2: 'value2' });
*/
async set(...args) {
const ssGuid = this.getSearchSessionGuid();
const store = storage_1.asyncLocalStorage.getStore();
const replay = store?.get('replay') ?? {};
if (ssGuid in replay) {
return Number(replay[ssGuid]);
}
const fields = {};
if (typeof args[0] === 'object') {
for (const [key, value] of Object.entries(args[0])) {
delete this.cachedFields[key];
fields[this.safeKey(key)] = value.toString();
}
}
else {
for (let i = 0; i < args.length; i += 2) {
const keyName = args[i];
delete this.cachedFields[keyName];
const key = this.safeKey(keyName);
const value = args[i + 1].toString();
fields[key] = value;
}
}
const fieldCount = await this.search.setFields(this.jobId, fields);
await this.search.setFields(this.jobId, {
[ssGuid]: fieldCount.toString(),
});
return fieldCount;
}
/**
* Returns the value of the record data field, given a field id
*
* @example
* const search = await workflow.search();
* const value = await search.get('field1');
*/
async get(id) {
try {
if (id in this.cachedFields) {
return this.cachedFields[id];
}
const value = await this.search.getField(this.jobId, this.safeKey(id));
this.cachedFields[id] = value;
return value;
}
catch (error) {
this.hotMeshClient.logger.error('meshflow-search-get-error', {
error,
});
return '';
}
}
/**
* Returns the values of all specified fields in the HASH stored at key.
*/
async mget(...args) {
let isCached = true;
const values = [];
const safeArgs = [];
for (let i = 0; i < args.length; i++) {
if (isCached && args[i] in this.cachedFields) {
values.push(this.cachedFields[args[i]]);
}
else {
isCached = false;
}
safeArgs.push(this.safeKey(args[i]));
}
try {
if (isCached) {
return values;
}
const returnValues = await this.search.getFields(this.jobId, safeArgs);
returnValues.forEach((value, index) => {
if (value !== null) {
this.cachedFields[args[index]] = value;
}
});
return returnValues;
}
catch (error) {
this.hotMeshClient.logger.error('meshflow-search-mget-error', {
error,
});
return [];
}
}
/**
* Deletes the fields provided as args. Returns the
* count of fields that were deleted.
*
* @example
* const search = await workflow.search();
* const count = await search.del('field1', 'field2', 'field3');
*/
async del(...args) {
const ssGuid = this.getSearchSessionGuid();
const store = storage_1.asyncLocalStorage.getStore();
const replay = store?.get('replay') ?? {};
const safeArgs = [];
for (let i = 0; i < args.length; i++) {
const keyName = args[i];
delete this.cachedFields[keyName];
safeArgs.push(this.safeKey(keyName));
}
if (ssGuid in replay) {
return Number(replay[ssGuid]);
}
const response = await this.search.deleteFields(this.jobId, safeArgs);
const formattedResponse = isNaN(response)
? 0
: Number(response);
await this.search.setFields(this.jobId, {
[ssGuid]: formattedResponse.toString(),
});
return formattedResponse;
}
/**
* Increments the value of a float field by the given amount. Returns the
* new value of the field after the increment. Pass a negative
* number to decrement the value.
*
* @example
* const search = await workflow.search();
* const count = await search.incr('field1', 1.5);
*/
async incr(key, val) {
delete this.cachedFields[key];
const ssGuid = this.getSearchSessionGuid();
const store = storage_1.asyncLocalStorage.getStore();
const replay = store?.get('replay') ?? {};
if (ssGuid in replay) {
return Number(replay[ssGuid]);
}
const num = await this.search.incrementFieldByFloat(this.jobId, this.safeKey(key), val);
await this.search.setFields(this.jobId, { [ssGuid]: num.toString() });
return num;
}
/**
* Multiplies the value of a field by the given amount. Returns the
* new value of the field after the multiplication. NOTE:
* this is exponential multiplication.
*
* @example
* const search = await workflow.search();
* const product = await search.mult('field1', 1.5);
*/
async mult(key, val) {
delete this.cachedFields[key];
const ssGuid = this.getSearchSessionGuid();
const store = storage_1.asyncLocalStorage.getStore();
const replay = store?.get('replay') ?? {};
if (ssGuid in replay) {
return Math.exp(Number(replay[ssGuid]));
}
const ssGuidValue = await this.search.incrementFieldByFloat(this.jobId, ssGuid, 1);
if (ssGuidValue === 1) {
const log = Math.log(val);
const logTotal = await this.search.incrementFieldByFloat(this.jobId, this.safeKey(key), log);
await this.search.setFields(this.jobId, {
[ssGuid]: logTotal.toString(),
});
return Math.exp(logTotal);
}
else {
const logTotalStr = await this.search.getField(this.jobId, ssGuid);
const logTotal = Number(logTotalStr);
return Math.exp(logTotal);
}
}
}
exports.Search = Search;