@hotmeshio/hotmesh
Version:
Permanent-Memory Workflows & AI Agents
348 lines (346 loc) • 13.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;
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('memflow-client-search-err', {
error,
});
}
}
}
/**
* Returns all user-defined attributes (udata) for a workflow.
* These are fields that start with underscore (_) and have type='udata'.
*
* @example
* ```typescript
* const allUserData = await Search.findAllUserData('job123', hotMeshClient);
* // Returns: { _status: "active", _counter: "42", _name: "test" }
* ```
*/
static async findAllUserData(jobId, hotMeshClient) {
const keyParams = {
appId: hotMeshClient.appId,
jobId: jobId,
};
const key = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
const search = hotMeshClient.engine.search;
const rawResult = await search.updateContext(key, {
'@udata:all': ''
});
// Transform the result:
// 1. Remove underscore prefix from keys
// 2. Handle special fields (like exponential values)
const result = {};
for (const [key, value] of Object.entries(rawResult)) {
// Remove underscore prefix
const cleanKey = key.startsWith('_') ? key.slice(1) : key;
// Special handling for fields that use logarithmic storage
if (cleanKey === 'multer') {
// Convert from log value back to actual value
const expValue = Math.exp(Number(value));
// Round to nearest integer since log multiplication doesn't need decimal precision
result[cleanKey] = Math.round(expValue).toString();
}
else {
result[cleanKey] = value;
}
}
return result;
}
/**
* 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('memflow-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]);
}
// Prepare fields to set with udata format
let udataFields;
if (typeof args[0] === 'object') {
// Object format: { field1: 'value1', field2: 'value2' }
udataFields = {};
for (const [key, value] of Object.entries(args[0])) {
udataFields[this.safeKey(key)] = value.toString();
}
}
else {
// Array format: ['field1', 'value1', 'field2', 'value2']
udataFields = [];
for (let i = 0; i < args.length; i += 2) {
const keyName = args[i];
const key = this.safeKey(keyName);
const value = args[i + 1].toString();
udataFields.push(key, value);
}
}
// Use single transactional call to update fields and store replay value
const result = await this.search.updateContext(this.jobId, {
'@udata:set': JSON.stringify(udataFields),
[ssGuid]: '', // Pass replay ID to hash module for transactional replay storage
});
return result;
}
/**
* 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) {
const ssGuid = this.getSearchSessionGuid();
const store = storage_1.asyncLocalStorage.getStore();
const replay = store?.get('replay') ?? {};
if (ssGuid in replay) {
// Replay cache stores the field value
return replay[ssGuid];
}
try {
// Use server-side udata get operation with replay storage
const result = await this.search.updateContext(this.jobId, {
'@udata:get': this.safeKey(id),
[ssGuid]: '', // Pass replay ID to hash module
});
return result || '';
}
catch (error) {
this.hotMeshClient.logger.error('memflow-search-get-error', {
error,
});
return '';
}
}
/**
* Returns the values of all specified fields in the HASH stored at key.
*/
async mget(...args) {
const ssGuid = this.getSearchSessionGuid();
const store = storage_1.asyncLocalStorage.getStore();
const replay = store?.get('replay') ?? {};
if (ssGuid in replay) {
// Replay cache stores the field values array
const replayValue = replay[ssGuid];
return typeof replayValue === 'string' ? replayValue.split('|||') : replayValue;
}
try {
const safeArgs = args.map(arg => this.safeKey(arg));
// Use server-side udata mget operation with replay storage
const result = await this.search.updateContext(this.jobId, {
'@udata:mget': JSON.stringify(safeArgs),
[ssGuid]: '', // Pass replay ID to hash module
});
return result || [];
}
catch (error) {
this.hotMeshClient.logger.error('memflow-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') ?? {};
if (ssGuid in replay) {
return Number(replay[ssGuid]);
}
const safeArgs = args.map(arg => this.safeKey(arg));
// Use server-side udata delete operation with replay storage
const result = await this.search.updateContext(this.jobId, {
'@udata:delete': JSON.stringify(safeArgs),
[ssGuid]: '', // Pass replay ID to hash module for transactional replay storage
});
return Number(result || 0);
}
/**
* 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) {
const ssGuid = this.getSearchSessionGuid();
const store = storage_1.asyncLocalStorage.getStore();
const replay = store?.get('replay') ?? {};
if (ssGuid in replay) {
return Number(replay[ssGuid]);
}
// Use server-side udata increment operation with replay storage
const result = await this.search.updateContext(this.jobId, {
'@udata:increment': JSON.stringify({ field: this.safeKey(key), value: val }),
[ssGuid]: '', // Pass replay ID to hash module for transactional replay storage
});
return Number(result);
}
/**
* 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) {
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]));
}
// Use server-side udata multiply operation with replay storage
const result = await this.search.updateContext(this.jobId, {
'@udata:multiply': JSON.stringify({ field: this.safeKey(key), value: val }),
[ssGuid]: '', // Pass replay ID to hash module for transactional replay storage
});
// The result is the log value, so we need to exponentiate it
return Math.exp(Number(result));
}
}
exports.Search = Search;