@memberjunction/server 5.38.0 → 5.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/apolloServer/index.d.ts +0 -8
- package/dist/apolloServer/index.d.ts.map +1 -1
- package/dist/apolloServer/index.js +61 -0
- package/dist/apolloServer/index.js.map +1 -1
- package/dist/config.d.ts +68 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2 -21
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +75 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +314 -1
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +51 -0
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -0
- package/dist/generic/FireAndForgetHeartbeat.js +44 -0
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -2
- package/dist/index.js.map +1 -1
- package/dist/logging/NoLog.d.ts +50 -0
- package/dist/logging/NoLog.d.ts.map +1 -0
- package/dist/logging/NoLog.js +80 -0
- package/dist/logging/NoLog.js.map +1 -0
- package/dist/logging/bootAudit.d.ts +43 -0
- package/dist/logging/bootAudit.d.ts.map +1 -0
- package/dist/logging/bootAudit.js +83 -0
- package/dist/logging/bootAudit.js.map +1 -0
- package/dist/logging/boundaryLogPayload.d.ts +18 -0
- package/dist/logging/boundaryLogPayload.d.ts.map +1 -0
- package/dist/logging/boundaryLogPayload.js +18 -0
- package/dist/logging/boundaryLogPayload.js.map +1 -0
- package/dist/logging/secretRedactor.d.ts +23 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -0
- package/dist/logging/secretRedactor.js +53 -0
- package/dist/logging/secretRedactor.js.map +1 -0
- package/dist/logging/shortenForLog.d.ts +8 -0
- package/dist/logging/shortenForLog.d.ts.map +1 -0
- package/dist/logging/shortenForLog.js +21 -0
- package/dist/logging/shortenForLog.js.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts +22 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.js +127 -0
- package/dist/logging/variablesLoggingMiddleware.js.map +1 -0
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +8 -4
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +259 -2
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +1276 -117
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/IntegrationProgressResolver.d.ts +90 -0
- package/dist/resolvers/IntegrationProgressResolver.d.ts.map +1 -0
- package/dist/resolvers/IntegrationProgressResolver.js +196 -0
- package/dist/resolvers/IntegrationProgressResolver.js.map +1 -0
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +19 -14
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +26 -5
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +12 -2
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SearchEntitiesResolver.d.ts +46 -0
- package/dist/resolvers/SearchEntitiesResolver.d.ts.map +1 -0
- package/dist/resolvers/SearchEntitiesResolver.js +216 -0
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -0
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -1
- package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
- package/dist/services/ScheduledJobsService.d.ts.map +1 -1
- package/dist/services/ScheduledJobsService.js +14 -2
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/package.json +75 -74
- package/src/__tests__/NoLog.test.ts +76 -0
- package/src/__tests__/bootAudit.test.ts +188 -0
- package/src/__tests__/boundaryLogPayload.test.ts +31 -0
- package/src/__tests__/getDataTokenRedaction.test.ts +84 -0
- package/src/__tests__/secretRedactor.test.ts +163 -0
- package/src/__tests__/subscriptionRedaction.test.ts +217 -0
- package/src/apolloServer/index.ts +58 -0
- package/src/config.ts +27 -0
- package/src/context.ts +2 -19
- package/src/generated/generated.ts +239 -1
- package/src/generic/FireAndForgetHeartbeat.ts +85 -0
- package/src/generic/__tests__/FireAndForgetHeartbeat.test.ts +99 -0
- package/src/index.ts +137 -2
- package/src/logging/NoLog.ts +88 -0
- package/src/logging/bootAudit.ts +117 -0
- package/src/logging/boundaryLogPayload.ts +19 -0
- package/src/logging/secretRedactor.ts +82 -0
- package/src/logging/shortenForLog.ts +17 -0
- package/src/logging/variablesLoggingMiddleware.ts +191 -0
- package/src/resolvers/GetDataResolver.ts +9 -5
- package/src/resolvers/IntegrationDiscoveryResolver.ts +1111 -120
- package/src/resolvers/IntegrationProgressResolver.ts +220 -0
- package/src/resolvers/MCPResolver.ts +18 -14
- package/src/resolvers/RunAIAgentResolver.ts +28 -5
- package/src/resolvers/RunTestResolver.ts +14 -2
- package/src/resolvers/SearchEntitiesResolver.ts +173 -0
- package/src/resolvers/VectorizeEntityResolver.ts +14 -1
- package/src/resolvers/__tests__/IntegrationProgressResolver.test.ts +170 -0
- package/src/services/ScheduledJobsService.ts +15 -2
|
@@ -15,6 +15,63 @@ import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
|
|
|
15
15
|
* @param servers - HTTP server and WebSocket cleanup disposable
|
|
16
16
|
* @param additionalPlugins - Optional additional plugins to merge with built-in plugins
|
|
17
17
|
*/
|
|
18
|
+
/**
|
|
19
|
+
* Apollo plugin: log every Integration* GraphQL operation to stdout as
|
|
20
|
+
* structured JSON. Lets operators tailing the MJAPI log see exactly which
|
|
21
|
+
* resolver the wizard / Explorer hit on each press, with arg names (values
|
|
22
|
+
* redacted for credentials). Diagnostic-only — no behavior change.
|
|
23
|
+
*
|
|
24
|
+
* Emits two events per operation:
|
|
25
|
+
* {"event":"gql.integration.request","method":"...","fieldName":"...","argNames":[...]}
|
|
26
|
+
* {"event":"gql.integration.response","method":"...","fieldName":"...","durationMs":N,"hasErrors":bool}
|
|
27
|
+
*
|
|
28
|
+
* Filter from the log:
|
|
29
|
+
* tail -f /tmp/mjapi.log | grep '"event":"gql\.integration\.'
|
|
30
|
+
*/
|
|
31
|
+
const integrationOperationTracer: ApolloServerPlugin = {
|
|
32
|
+
async requestDidStart(requestCtx) {
|
|
33
|
+
// Off by default — the plugin stays registered but emits nothing unless the
|
|
34
|
+
// MJ_INTEGRATION_TRACE debug flag is explicitly set. Avoids logging on every
|
|
35
|
+
// request in normal operation.
|
|
36
|
+
if (process.env.MJ_INTEGRATION_TRACE !== 'true') return undefined;
|
|
37
|
+
const opName = requestCtx.request.operationName ?? 'anonymous';
|
|
38
|
+
const query = requestCtx.request.query ?? '';
|
|
39
|
+
// Lightweight match — fire only for operations that touch an Integration
|
|
40
|
+
// resolver field. Avoid logging every Color / User / Task query.
|
|
41
|
+
if (!query.includes('Integration')) return undefined;
|
|
42
|
+
const startedAt = Date.now();
|
|
43
|
+
return {
|
|
44
|
+
async willSendResponse(rc) {
|
|
45
|
+
// Walk the fields the operation queried; only those starting with
|
|
46
|
+
// "Integration" matter. Variables surfaced as keys only.
|
|
47
|
+
const op = rc.operation;
|
|
48
|
+
if (!op) return;
|
|
49
|
+
const integrationFields: string[] = [];
|
|
50
|
+
for (const sel of op.selectionSet.selections) {
|
|
51
|
+
if (sel.kind === 'Field' && sel.name.value.startsWith('Integration')) {
|
|
52
|
+
integrationFields.push(sel.name.value);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (integrationFields.length === 0) return;
|
|
56
|
+
const argNames = Object.keys(rc.request.variables ?? {});
|
|
57
|
+
const durationMs = Date.now() - startedAt;
|
|
58
|
+
const hasErrors = !!(rc.response.body.kind === 'single' && rc.response.body.singleResult.errors?.length);
|
|
59
|
+
for (const field of integrationFields) {
|
|
60
|
+
console.log(JSON.stringify({
|
|
61
|
+
ts: new Date().toISOString(),
|
|
62
|
+
event: 'gql.integration.response',
|
|
63
|
+
operationName: opName,
|
|
64
|
+
method: field,
|
|
65
|
+
argNames,
|
|
66
|
+
durationMs,
|
|
67
|
+
hasErrors,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
18
75
|
const buildApolloServer = (
|
|
19
76
|
configOverride: ApolloServerOptions<AppContext>,
|
|
20
77
|
{ httpServer, serverCleanup }: { httpServer: Server; serverCleanup: Disposable },
|
|
@@ -22,6 +79,7 @@ const buildApolloServer = (
|
|
|
22
79
|
) => {
|
|
23
80
|
const builtInPlugins: ApolloServerPlugin[] = [
|
|
24
81
|
ApolloServerPluginDrainHttpServer({ httpServer }),
|
|
82
|
+
integrationOperationTracer,
|
|
25
83
|
{
|
|
26
84
|
async serverWillStart() {
|
|
27
85
|
return {
|
package/src/config.ts
CHANGED
|
@@ -205,6 +205,21 @@ const cacheSettingsSchema = z.object({
|
|
|
205
205
|
verboseLogging: z.boolean().optional().default(false),
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
+
const loggingSettingsSchema = z.object({
|
|
209
|
+
graphql: z.object({
|
|
210
|
+
/**
|
|
211
|
+
* When true, emit a redacted variables block per root resolver call via the
|
|
212
|
+
* type-graphql global middleware. Default: false in all environments regardless
|
|
213
|
+
* of NODE_ENV. Env override: `MJ_LOG_GRAPHQL_VARIABLES`.
|
|
214
|
+
*
|
|
215
|
+
* SECURITY: this is an opt-in verbose-echo path for developers debugging locally.
|
|
216
|
+
* The always-on request log line in `context.ts` does NOT emit variables — that
|
|
217
|
+
* is the load-bearing leak fix. This flag is additive on top of the always-on log.
|
|
218
|
+
*/
|
|
219
|
+
logVariables: z.boolean().optional().default(false),
|
|
220
|
+
}).optional().default({}),
|
|
221
|
+
});
|
|
222
|
+
|
|
208
223
|
const feedbackGithubSettingsSchema = z.object({
|
|
209
224
|
owner: z.string().optional(),
|
|
210
225
|
repo: z.string().optional(),
|
|
@@ -236,6 +251,7 @@ const configInfoSchema = z.object({
|
|
|
236
251
|
multiTenancy: multiTenancySchema.optional().default({}),
|
|
237
252
|
serverExtensions: z.array(serverExtensionSchema).optional().default([]),
|
|
238
253
|
cacheSettings: cacheSettingsSchema.optional().default({}),
|
|
254
|
+
loggingSettings: loggingSettingsSchema.optional().default({}),
|
|
239
255
|
feedbackSettings: feedbackSettingsSchema.optional().default({}),
|
|
240
256
|
|
|
241
257
|
apiKey: z.string().optional(),
|
|
@@ -284,6 +300,7 @@ export type QueryDialectConfig = z.infer<typeof queryDialectSchema>;
|
|
|
284
300
|
export type MultiTenancyConfig = z.infer<typeof multiTenancySchema>;
|
|
285
301
|
export type ServerExtensionConfig = z.infer<typeof serverExtensionSchema>;
|
|
286
302
|
export type CacheSettingsConfig = z.infer<typeof cacheSettingsSchema>;
|
|
303
|
+
export type LoggingSettingsConfig = z.infer<typeof loggingSettingsSchema>;
|
|
287
304
|
export type FeedbackGithubSettingsConfig = z.infer<typeof feedbackGithubSettingsSchema>;
|
|
288
305
|
export type FeedbackSettingsConfig = z.infer<typeof feedbackSettingsSchema>;
|
|
289
306
|
export type ConfigInfo = z.infer<typeof configInfoSchema>;
|
|
@@ -421,6 +438,16 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
|
|
|
421
438
|
verboseLogging: false,
|
|
422
439
|
},
|
|
423
440
|
|
|
441
|
+
// Logging settings defaults — variables logging is always off unless the operator
|
|
442
|
+
// sets MJ_LOG_GRAPHQL_VARIABLES=true (or sets logVariables in mj.config.cjs).
|
|
443
|
+
// NOTE: this only governs the opt-in verbose-echo middleware. The always-on request
|
|
444
|
+
// log in context.ts already strips variables unconditionally.
|
|
445
|
+
loggingSettings: {
|
|
446
|
+
graphql: {
|
|
447
|
+
logVariables: parseBooleanEnv(process.env.MJ_LOG_GRAPHQL_VARIABLES),
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
|
|
424
451
|
// Auth providers (environment-driven)
|
|
425
452
|
authProviders: [
|
|
426
453
|
// Microsoft Azure AD / Entra ID
|
package/src/context.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { getSigningKeys, getSystemUser, getValidationOptions, verifyUserRecord,
|
|
|
9
9
|
import { TokenExpiredError, AuthProviderFactory } from '@memberjunction/auth-providers';
|
|
10
10
|
import { authCache } from './cache.js';
|
|
11
11
|
import { userEmailMap, apiKey, mj_core_schema } from './config.js';
|
|
12
|
+
import { buildBoundaryLogPayload } from './logging/boundaryLogPayload.js';
|
|
12
13
|
import { DataSourceInfo, UserPayload } from './types.js';
|
|
13
14
|
import { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
|
|
14
15
|
import { v4 as uuidv4 } from 'uuid';
|
|
@@ -21,24 +22,6 @@ import { UUIDsEqual } from '@memberjunction/global';
|
|
|
21
22
|
import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
|
|
22
23
|
import { GetAPIKeyEngine } from '@memberjunction/api-keys';
|
|
23
24
|
|
|
24
|
-
/**
|
|
25
|
-
* Renders a value for one-line console logging without Node's `[Object]` truncation.
|
|
26
|
-
* Arrays keep their structure; non-array objects collapse to JSON, truncated at `maxLen`.
|
|
27
|
-
* Objects whose JSON exceeds `maxLen` and contain nested structure are recursed into so
|
|
28
|
-
* outer keys remain visible.
|
|
29
|
-
*/
|
|
30
|
-
function shortenForLog(value: unknown, maxLen = 300): unknown {
|
|
31
|
-
if (value === null || typeof value !== 'object') return value;
|
|
32
|
-
if (Array.isArray(value)) return value.map((v) => shortenForLog(v, maxLen));
|
|
33
|
-
const json = JSON.stringify(value);
|
|
34
|
-
if (json.length <= maxLen) return json;
|
|
35
|
-
const result: Record<string, unknown> = {};
|
|
36
|
-
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
37
|
-
result[k] = shortenForLog(v, maxLen);
|
|
38
|
-
}
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
25
|
const verifyAsync = async (issuer: string, token: string): Promise<jwt.JwtPayload> =>
|
|
43
26
|
new Promise((resolve, reject) => {
|
|
44
27
|
const options = getValidationOptions(issuer);
|
|
@@ -342,7 +325,7 @@ export const contextFunction =
|
|
|
342
325
|
const reqAny = req as any;
|
|
343
326
|
const operationName: string | undefined = reqAny.body?.operationName;
|
|
344
327
|
if (operationName !== 'IntrospectionQuery') {
|
|
345
|
-
console.dir(
|
|
328
|
+
console.dir(buildBoundaryLogPayload(operationName), { depth: null, breakLength: 200 });
|
|
346
329
|
}
|
|
347
330
|
|
|
348
331
|
// Auth already happened in the unified auth middleware — just read the result
|
|
@@ -7593,6 +7593,12 @@ each time the agent processes a prompt step.`})
|
|
|
7593
7593
|
@MaxLength(36)
|
|
7594
7594
|
CompanyID?: string;
|
|
7595
7595
|
|
|
7596
|
+
@Field(() => Int, {nullable: true, description: `Total input tokens served from the AI provider's prompt cache (cache reads / hits) across this agent run, summed from child prompt runs' TokensCacheReadRollup and sub-agent runs' TotalCacheReadTokensUsed. Counts only; the cost impact (cache reads are billed at a steep discount) is reflected in TotalCost. The cache counterpart of TotalPromptTokensUsed.`})
|
|
7597
|
+
TotalCacheReadTokensUsed?: number;
|
|
7598
|
+
|
|
7599
|
+
@Field(() => Int, {nullable: true, description: `Total input tokens written to the AI provider's prompt cache (cache writes / creation) across this agent run, summed from child prompt runs' TokensCacheWriteRollup and sub-agent runs' TotalCacheWriteTokensUsed. Populated for providers that bill cache creation (e.g. Anthropic); 0 or NULL otherwise. The cache counterpart of TotalCompletionTokensUsed.`})
|
|
7600
|
+
TotalCacheWriteTokensUsed?: number;
|
|
7601
|
+
|
|
7596
7602
|
@Field({nullable: true})
|
|
7597
7603
|
@MaxLength(255)
|
|
7598
7604
|
Agent?: string;
|
|
@@ -7814,6 +7820,12 @@ export class CreateMJAIAgentRunInput {
|
|
|
7814
7820
|
@Field({ nullable: true })
|
|
7815
7821
|
CompanyID: string | null;
|
|
7816
7822
|
|
|
7823
|
+
@Field(() => Int, { nullable: true })
|
|
7824
|
+
TotalCacheReadTokensUsed: number | null;
|
|
7825
|
+
|
|
7826
|
+
@Field(() => Int, { nullable: true })
|
|
7827
|
+
TotalCacheWriteTokensUsed: number | null;
|
|
7828
|
+
|
|
7817
7829
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
7818
7830
|
RestoreContext___?: RestoreContextInput;
|
|
7819
7831
|
}
|
|
@@ -7956,6 +7968,12 @@ export class UpdateMJAIAgentRunInput {
|
|
|
7956
7968
|
@Field({ nullable: true })
|
|
7957
7969
|
CompanyID?: string | null;
|
|
7958
7970
|
|
|
7971
|
+
@Field(() => Int, { nullable: true })
|
|
7972
|
+
TotalCacheReadTokensUsed?: number | null;
|
|
7973
|
+
|
|
7974
|
+
@Field(() => Int, { nullable: true })
|
|
7975
|
+
TotalCacheWriteTokensUsed?: number | null;
|
|
7976
|
+
|
|
7959
7977
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
7960
7978
|
OldValues___?: KeyValuePairInput[];
|
|
7961
7979
|
|
|
@@ -12243,6 +12261,12 @@ export class MJAIModelCost_ {
|
|
|
12243
12261
|
@Field()
|
|
12244
12262
|
_mj__UpdatedAt: Date;
|
|
12245
12263
|
|
|
12264
|
+
@Field(() => Float, {nullable: true, description: `Optional price per unit for input tokens served from the AI provider's prompt cache (cache reads / hits), expressed in the same currency and UnitType (e.g. per 1M tokens) as InputPricePerUnit. When NULL, cache-read tokens are priced at InputPricePerUnit. Cache reads are usually far cheaper than uncached input (e.g. ~0.1x for Anthropic/Gemini, ~0.5x for OpenAI).`})
|
|
12265
|
+
CacheReadPricePerUnit?: number;
|
|
12266
|
+
|
|
12267
|
+
@Field(() => Float, {nullable: true, description: `Optional price per unit for input tokens written to the AI provider's prompt cache (cache writes / creation), expressed in the same currency and UnitType as InputPricePerUnit. When NULL, cache-write tokens are priced at InputPricePerUnit. Populated for providers that bill cache creation separately (e.g. Anthropic, ~1.25x input); leave NULL for providers that do not (OpenAI, Gemini), which also report 0 cache-write tokens.`})
|
|
12268
|
+
CacheWritePricePerUnit?: number;
|
|
12269
|
+
|
|
12246
12270
|
@Field()
|
|
12247
12271
|
@MaxLength(50)
|
|
12248
12272
|
Model: string;
|
|
@@ -12305,6 +12329,12 @@ export class CreateMJAIModelCostInput {
|
|
|
12305
12329
|
@Field({ nullable: true })
|
|
12306
12330
|
Comments: string | null;
|
|
12307
12331
|
|
|
12332
|
+
@Field(() => Float, { nullable: true })
|
|
12333
|
+
CacheReadPricePerUnit: number | null;
|
|
12334
|
+
|
|
12335
|
+
@Field(() => Float, { nullable: true })
|
|
12336
|
+
CacheWritePricePerUnit: number | null;
|
|
12337
|
+
|
|
12308
12338
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
12309
12339
|
RestoreContext___?: RestoreContextInput;
|
|
12310
12340
|
}
|
|
@@ -12354,6 +12384,12 @@ export class UpdateMJAIModelCostInput {
|
|
|
12354
12384
|
@Field({ nullable: true })
|
|
12355
12385
|
Comments?: string | null;
|
|
12356
12386
|
|
|
12387
|
+
@Field(() => Float, { nullable: true })
|
|
12388
|
+
CacheReadPricePerUnit?: number | null;
|
|
12389
|
+
|
|
12390
|
+
@Field(() => Float, { nullable: true })
|
|
12391
|
+
CacheWritePricePerUnit?: number | null;
|
|
12392
|
+
|
|
12357
12393
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
12358
12394
|
OldValues___?: KeyValuePairInput[];
|
|
12359
12395
|
|
|
@@ -15236,6 +15272,18 @@ export class MJAIPromptRun_ {
|
|
|
15236
15272
|
@Field({nullable: true, description: `The assistant prefill text that was used during this prompt execution. Records whether native prefill or fallback was applied. NULL means no prefill was used.`})
|
|
15237
15273
|
AssistantPrefill?: string;
|
|
15238
15274
|
|
|
15275
|
+
@Field(() => Int, {nullable: true, description: `Number of input tokens served from the AI provider's prompt cache (a cache READ / hit) for this run, as reported by the provider. Counts only; no cost is derived here. NULL if the provider did not report cache reads or caching did not engage. Distinct from CacheHit/CacheKey, which track MemberJunction's own result cache.`})
|
|
15276
|
+
TokensCacheRead?: number;
|
|
15277
|
+
|
|
15278
|
+
@Field(() => Int, {nullable: true, description: `Number of input tokens written to the AI provider's prompt cache (a cache WRITE / creation) for this run, as reported by the provider. Populated for providers that report cache writes (e.g. Anthropic cache_creation_input_tokens); NULL or 0 for providers that do not bill/report writes (OpenAI, Gemini, Groq, Cerebras). Counts only; no cost is derived here.`})
|
|
15279
|
+
TokensCacheWrite?: number;
|
|
15280
|
+
|
|
15281
|
+
@Field(() => Int, {nullable: true, description: `Rollup of TokensCacheRead across this prompt run and all of its descendant prompt runs (e.g. the individual attempts behind a parallel / multi-attempt / failover consolidation). For a leaf run this equals TokensCacheRead. Use this (not TokensCacheRead) when aggregating cache reads up a prompt-run or agent-run hierarchy so fan-out provider calls are not under-counted.`})
|
|
15282
|
+
TokensCacheReadRollup?: number;
|
|
15283
|
+
|
|
15284
|
+
@Field(() => Int, {nullable: true, description: `Rollup of TokensCacheWrite across this prompt run and all of its descendant prompt runs. For a leaf run this equals TokensCacheWrite. Mirrors TokensUsedRollup/TokensPromptRollup; populated for providers that report cache writes (e.g. Anthropic), otherwise 0 or NULL.`})
|
|
15285
|
+
TokensCacheWriteRollup?: number;
|
|
15286
|
+
|
|
15239
15287
|
@Field()
|
|
15240
15288
|
@MaxLength(255)
|
|
15241
15289
|
Prompt: string;
|
|
@@ -15563,6 +15611,18 @@ export class CreateMJAIPromptRunInput {
|
|
|
15563
15611
|
@Field({ nullable: true })
|
|
15564
15612
|
AssistantPrefill: string | null;
|
|
15565
15613
|
|
|
15614
|
+
@Field(() => Int, { nullable: true })
|
|
15615
|
+
TokensCacheRead: number | null;
|
|
15616
|
+
|
|
15617
|
+
@Field(() => Int, { nullable: true })
|
|
15618
|
+
TokensCacheWrite: number | null;
|
|
15619
|
+
|
|
15620
|
+
@Field(() => Int, { nullable: true })
|
|
15621
|
+
TokensCacheReadRollup: number | null;
|
|
15622
|
+
|
|
15623
|
+
@Field(() => Int, { nullable: true })
|
|
15624
|
+
TokensCacheWriteRollup: number | null;
|
|
15625
|
+
|
|
15566
15626
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
15567
15627
|
RestoreContext___?: RestoreContextInput;
|
|
15568
15628
|
}
|
|
@@ -15822,6 +15882,18 @@ export class UpdateMJAIPromptRunInput {
|
|
|
15822
15882
|
@Field({ nullable: true })
|
|
15823
15883
|
AssistantPrefill?: string | null;
|
|
15824
15884
|
|
|
15885
|
+
@Field(() => Int, { nullable: true })
|
|
15886
|
+
TokensCacheRead?: number | null;
|
|
15887
|
+
|
|
15888
|
+
@Field(() => Int, { nullable: true })
|
|
15889
|
+
TokensCacheWrite?: number | null;
|
|
15890
|
+
|
|
15891
|
+
@Field(() => Int, { nullable: true })
|
|
15892
|
+
TokensCacheReadRollup?: number | null;
|
|
15893
|
+
|
|
15894
|
+
@Field(() => Int, { nullable: true })
|
|
15895
|
+
TokensCacheWriteRollup?: number | null;
|
|
15896
|
+
|
|
15825
15897
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
15826
15898
|
OldValues___?: KeyValuePairInput[];
|
|
15827
15899
|
|
|
@@ -44228,7 +44300,7 @@ export class MJEntityField_ {
|
|
|
44228
44300
|
@MaxLength(20)
|
|
44229
44301
|
ValueListType: string;
|
|
44230
44302
|
|
|
44231
|
-
@Field({nullable: true, description: `Defines extended behaviors for a field such as
|
|
44303
|
+
@Field({nullable: true, description: `Defines extended behaviors for a field such as Email, Web URLs, Code, Markdown, HTML, and Icon. When set to 'Icon', the field's values are treated as icon CSS classes (e.g. Font Awesome) for per-row display in the UI.`})
|
|
44232
44304
|
@MaxLength(50)
|
|
44233
44305
|
ExtendedType?: string;
|
|
44234
44306
|
|
|
@@ -49627,6 +49699,10 @@ export class MJIntegrationObjectField_ {
|
|
|
49627
49699
|
@Field(() => Boolean, {description: `When true, this field was dynamically discovered by IntrospectSchema and is not defined in static connector metadata.`})
|
|
49628
49700
|
IsCustom: boolean;
|
|
49629
49701
|
|
|
49702
|
+
@Field({description: `Provenance of this IntegrationObjectField row: Declared (from static research/docs), Discovered (from runtime API introspection), Custom (customer-defined custom field, e.g., HubSpot custom property on standard object). Drives merge precedence — discovered/runtime wins for type/constraints; declared wins for description/label/sequence/category.`})
|
|
49703
|
+
@MaxLength(20)
|
|
49704
|
+
MetadataSource: string;
|
|
49705
|
+
|
|
49630
49706
|
@Field()
|
|
49631
49707
|
@MaxLength(255)
|
|
49632
49708
|
IntegrationObject: string;
|
|
@@ -49708,6 +49784,9 @@ export class CreateMJIntegrationObjectFieldInput {
|
|
|
49708
49784
|
@Field(() => Boolean, { nullable: true })
|
|
49709
49785
|
IsCustom?: boolean;
|
|
49710
49786
|
|
|
49787
|
+
@Field({ nullable: true })
|
|
49788
|
+
MetadataSource?: string;
|
|
49789
|
+
|
|
49711
49790
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
49712
49791
|
RestoreContext___?: RestoreContextInput;
|
|
49713
49792
|
}
|
|
@@ -49784,6 +49863,9 @@ export class UpdateMJIntegrationObjectFieldInput {
|
|
|
49784
49863
|
@Field(() => Boolean, { nullable: true })
|
|
49785
49864
|
IsCustom?: boolean;
|
|
49786
49865
|
|
|
49866
|
+
@Field({ nullable: true })
|
|
49867
|
+
MetadataSource?: string;
|
|
49868
|
+
|
|
49787
49869
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
49788
49870
|
OldValues___?: KeyValuePairInput[];
|
|
49789
49871
|
|
|
@@ -49963,6 +50045,59 @@ export class MJIntegrationObject_ {
|
|
|
49963
50045
|
@Field(() => Boolean, {description: `When true, this object was dynamically discovered by IntrospectSchema and is not defined in static connector metadata.`})
|
|
49964
50046
|
IsCustom: boolean;
|
|
49965
50047
|
|
|
50048
|
+
@Field({nullable: true, description: `HTTP path template for create operations. Generic CRUD in BaseRESTIntegrationConnector substitutes parent IDs into {var} placeholders. NULL means create not supported via metadata-driven path.`})
|
|
50049
|
+
CreateAPIPath?: string;
|
|
50050
|
+
|
|
50051
|
+
@Field({nullable: true, description: `HTTP method for create (typically POST). NULL means create not supported via metadata-driven path.`})
|
|
50052
|
+
@MaxLength(20)
|
|
50053
|
+
CreateMethod?: string;
|
|
50054
|
+
|
|
50055
|
+
@Field({nullable: true, description: `Request body shape for create: flat (top-level fields), wrapped (under CreateBodyKey), or literal (connector overrides CreateRecord and supplies own body).`})
|
|
50056
|
+
@MaxLength(50)
|
|
50057
|
+
CreateBodyShape?: string;
|
|
50058
|
+
|
|
50059
|
+
@Field({nullable: true, description: `Wrapper key for create body when CreateBodyShape=wrapped. Example: 'member' for YourMembership which wraps body as {member:{...}}.`})
|
|
50060
|
+
@MaxLength(100)
|
|
50061
|
+
CreateBodyKey?: string;
|
|
50062
|
+
|
|
50063
|
+
@Field({nullable: true, description: `Where the created record ID is found in the create response: path (URL of returned Location header), body (parsed from JSON response), header (specific named header).`})
|
|
50064
|
+
@MaxLength(20)
|
|
50065
|
+
CreateIDLocation?: string;
|
|
50066
|
+
|
|
50067
|
+
@Field({nullable: true, description: `HTTP path template for update operations. Typically contains {ID} placeholder substituted with the record ExternalID at runtime.`})
|
|
50068
|
+
UpdateAPIPath?: string;
|
|
50069
|
+
|
|
50070
|
+
@Field({nullable: true, description: `HTTP method for update (typically PATCH or PUT).`})
|
|
50071
|
+
@MaxLength(20)
|
|
50072
|
+
UpdateMethod?: string;
|
|
50073
|
+
|
|
50074
|
+
@Field({nullable: true, description: `Request body shape for update: flat | wrapped | literal. See CreateBodyShape.`})
|
|
50075
|
+
@MaxLength(50)
|
|
50076
|
+
UpdateBodyShape?: string;
|
|
50077
|
+
|
|
50078
|
+
@Field({nullable: true, description: `Wrapper key for update body when UpdateBodyShape=wrapped.`})
|
|
50079
|
+
@MaxLength(100)
|
|
50080
|
+
UpdateBodyKey?: string;
|
|
50081
|
+
|
|
50082
|
+
@Field({nullable: true, description: `For update: where the target record ID is located in the request — typically 'path' (substituted into UpdateAPIPath URL template).`})
|
|
50083
|
+
@MaxLength(20)
|
|
50084
|
+
UpdateIDLocation?: string;
|
|
50085
|
+
|
|
50086
|
+
@Field({nullable: true, description: `HTTP path template for delete operations. Typically contains {ID} placeholder. NULL means delete not supported via metadata-driven path. (Existing DeleteMethod column carries the verb.)`})
|
|
50087
|
+
DeleteAPIPath?: string;
|
|
50088
|
+
|
|
50089
|
+
@Field({nullable: true, description: `For delete: where the target record ID is located — typically 'path'.`})
|
|
50090
|
+
@MaxLength(20)
|
|
50091
|
+
DeleteIDLocation?: string;
|
|
50092
|
+
|
|
50093
|
+
@Field({nullable: true, description: `Vendor field name marking "last changed" — drives incremental sync filter when SupportsIncrementalSync=1. The exact filter syntax (e.g., $filter=Modified gt {value} or modified_since={value}) lives in Configuration.incrementalFilterFormat. Provable-only: leave NULL if docs do not name a watermark field.`})
|
|
50094
|
+
@MaxLength(255)
|
|
50095
|
+
IncrementalWatermarkField?: string;
|
|
50096
|
+
|
|
50097
|
+
@Field({description: `Provenance of this IntegrationObject row: Declared (from static research/docs), Discovered (from runtime API introspection like Salesforce /describe), Custom (genuinely customer-created, e.g., HubSpot custom objects). Drives merge precedence in IntegrationSchemaSync.`})
|
|
50098
|
+
@MaxLength(20)
|
|
50099
|
+
MetadataSource: string;
|
|
50100
|
+
|
|
49966
50101
|
@Field()
|
|
49967
50102
|
@MaxLength(100)
|
|
49968
50103
|
Integration: string;
|
|
@@ -50043,6 +50178,48 @@ export class CreateMJIntegrationObjectInput {
|
|
|
50043
50178
|
@Field(() => Boolean, { nullable: true })
|
|
50044
50179
|
IsCustom?: boolean;
|
|
50045
50180
|
|
|
50181
|
+
@Field({ nullable: true })
|
|
50182
|
+
CreateAPIPath: string | null;
|
|
50183
|
+
|
|
50184
|
+
@Field({ nullable: true })
|
|
50185
|
+
CreateMethod: string | null;
|
|
50186
|
+
|
|
50187
|
+
@Field({ nullable: true })
|
|
50188
|
+
CreateBodyShape: string | null;
|
|
50189
|
+
|
|
50190
|
+
@Field({ nullable: true })
|
|
50191
|
+
CreateBodyKey: string | null;
|
|
50192
|
+
|
|
50193
|
+
@Field({ nullable: true })
|
|
50194
|
+
CreateIDLocation: string | null;
|
|
50195
|
+
|
|
50196
|
+
@Field({ nullable: true })
|
|
50197
|
+
UpdateAPIPath: string | null;
|
|
50198
|
+
|
|
50199
|
+
@Field({ nullable: true })
|
|
50200
|
+
UpdateMethod: string | null;
|
|
50201
|
+
|
|
50202
|
+
@Field({ nullable: true })
|
|
50203
|
+
UpdateBodyShape: string | null;
|
|
50204
|
+
|
|
50205
|
+
@Field({ nullable: true })
|
|
50206
|
+
UpdateBodyKey: string | null;
|
|
50207
|
+
|
|
50208
|
+
@Field({ nullable: true })
|
|
50209
|
+
UpdateIDLocation: string | null;
|
|
50210
|
+
|
|
50211
|
+
@Field({ nullable: true })
|
|
50212
|
+
DeleteAPIPath: string | null;
|
|
50213
|
+
|
|
50214
|
+
@Field({ nullable: true })
|
|
50215
|
+
DeleteIDLocation: string | null;
|
|
50216
|
+
|
|
50217
|
+
@Field({ nullable: true })
|
|
50218
|
+
IncrementalWatermarkField: string | null;
|
|
50219
|
+
|
|
50220
|
+
@Field({ nullable: true })
|
|
50221
|
+
MetadataSource?: string;
|
|
50222
|
+
|
|
50046
50223
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
50047
50224
|
RestoreContext___?: RestoreContextInput;
|
|
50048
50225
|
}
|
|
@@ -50116,6 +50293,48 @@ export class UpdateMJIntegrationObjectInput {
|
|
|
50116
50293
|
@Field(() => Boolean, { nullable: true })
|
|
50117
50294
|
IsCustom?: boolean;
|
|
50118
50295
|
|
|
50296
|
+
@Field({ nullable: true })
|
|
50297
|
+
CreateAPIPath?: string | null;
|
|
50298
|
+
|
|
50299
|
+
@Field({ nullable: true })
|
|
50300
|
+
CreateMethod?: string | null;
|
|
50301
|
+
|
|
50302
|
+
@Field({ nullable: true })
|
|
50303
|
+
CreateBodyShape?: string | null;
|
|
50304
|
+
|
|
50305
|
+
@Field({ nullable: true })
|
|
50306
|
+
CreateBodyKey?: string | null;
|
|
50307
|
+
|
|
50308
|
+
@Field({ nullable: true })
|
|
50309
|
+
CreateIDLocation?: string | null;
|
|
50310
|
+
|
|
50311
|
+
@Field({ nullable: true })
|
|
50312
|
+
UpdateAPIPath?: string | null;
|
|
50313
|
+
|
|
50314
|
+
@Field({ nullable: true })
|
|
50315
|
+
UpdateMethod?: string | null;
|
|
50316
|
+
|
|
50317
|
+
@Field({ nullable: true })
|
|
50318
|
+
UpdateBodyShape?: string | null;
|
|
50319
|
+
|
|
50320
|
+
@Field({ nullable: true })
|
|
50321
|
+
UpdateBodyKey?: string | null;
|
|
50322
|
+
|
|
50323
|
+
@Field({ nullable: true })
|
|
50324
|
+
UpdateIDLocation?: string | null;
|
|
50325
|
+
|
|
50326
|
+
@Field({ nullable: true })
|
|
50327
|
+
DeleteAPIPath?: string | null;
|
|
50328
|
+
|
|
50329
|
+
@Field({ nullable: true })
|
|
50330
|
+
DeleteIDLocation?: string | null;
|
|
50331
|
+
|
|
50332
|
+
@Field({ nullable: true })
|
|
50333
|
+
IncrementalWatermarkField?: string | null;
|
|
50334
|
+
|
|
50335
|
+
@Field({ nullable: true })
|
|
50336
|
+
MetadataSource?: string;
|
|
50337
|
+
|
|
50119
50338
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
50120
50339
|
OldValues___?: KeyValuePairInput[];
|
|
50121
50340
|
|
|
@@ -65639,6 +65858,9 @@ export class MJScheduledJob_ {
|
|
|
65639
65858
|
@Field()
|
|
65640
65859
|
_mj__UpdatedAt: Date;
|
|
65641
65860
|
|
|
65861
|
+
@Field(() => Boolean, {description: `When true AND LastRunAt IS NULL, the scheduler sets NextRunAt to now() instead of the next cron tick on initialization, so the job runs on the next polling cycle. Useful for newly-seeded jobs that should not wait up to a full cron interval before their first execution.`})
|
|
65862
|
+
RunImmediatelyIfNeverRun: boolean;
|
|
65863
|
+
|
|
65642
65864
|
@Field()
|
|
65643
65865
|
@MaxLength(100)
|
|
65644
65866
|
JobType: string;
|
|
@@ -65742,6 +65964,9 @@ export class CreateMJScheduledJobInput {
|
|
|
65742
65964
|
@Field({ nullable: true })
|
|
65743
65965
|
ConcurrencyMode?: string;
|
|
65744
65966
|
|
|
65967
|
+
@Field(() => Boolean, { nullable: true })
|
|
65968
|
+
RunImmediatelyIfNeverRun?: boolean;
|
|
65969
|
+
|
|
65745
65970
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
65746
65971
|
RestoreContext___?: RestoreContextInput;
|
|
65747
65972
|
}
|
|
@@ -65830,6 +66055,9 @@ export class UpdateMJScheduledJobInput {
|
|
|
65830
66055
|
@Field({ nullable: true })
|
|
65831
66056
|
ConcurrencyMode?: string;
|
|
65832
66057
|
|
|
66058
|
+
@Field(() => Boolean, { nullable: true })
|
|
66059
|
+
RunImmediatelyIfNeverRun?: boolean;
|
|
66060
|
+
|
|
65833
66061
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
65834
66062
|
OldValues___?: KeyValuePairInput[];
|
|
65835
66063
|
|
|
@@ -69621,6 +69849,10 @@ export class MJTagSynonym_ {
|
|
|
69621
69849
|
@Field()
|
|
69622
69850
|
_mj__UpdatedAt: Date;
|
|
69623
69851
|
|
|
69852
|
+
@Field({description: `Approval state of the synonym. Active = resolves to its tag during classification. Pending = proposed (e.g. by the LLM or a bulk import) and awaiting human review; does not resolve until approved. Rejected = reviewed and declined; retained for audit and to suppress re-proposal.`})
|
|
69853
|
+
@MaxLength(20)
|
|
69854
|
+
Status: string;
|
|
69855
|
+
|
|
69624
69856
|
@Field()
|
|
69625
69857
|
@MaxLength(255)
|
|
69626
69858
|
Tag: string;
|
|
@@ -69644,6 +69876,9 @@ export class CreateMJTagSynonymInput {
|
|
|
69644
69876
|
@Field({ nullable: true })
|
|
69645
69877
|
Source?: string;
|
|
69646
69878
|
|
|
69879
|
+
@Field({ nullable: true })
|
|
69880
|
+
Status?: string;
|
|
69881
|
+
|
|
69647
69882
|
@Field(() => RestoreContextInput, { nullable: true })
|
|
69648
69883
|
RestoreContext___?: RestoreContextInput;
|
|
69649
69884
|
}
|
|
@@ -69666,6 +69901,9 @@ export class UpdateMJTagSynonymInput {
|
|
|
69666
69901
|
@Field({ nullable: true })
|
|
69667
69902
|
Source?: string;
|
|
69668
69903
|
|
|
69904
|
+
@Field({ nullable: true })
|
|
69905
|
+
Status?: string;
|
|
69906
|
+
|
|
69669
69907
|
@Field(() => [KeyValuePairInput], { nullable: true })
|
|
69670
69908
|
OldValues___?: KeyValuePairInput[];
|
|
69671
69909
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { PubSubEngine } from 'type-graphql';
|
|
2
|
+
import { LogError } from '@memberjunction/core';
|
|
3
|
+
import { PUSH_STATUS_UPDATES_TOPIC } from './PushStatusResolver.js';
|
|
4
|
+
|
|
5
|
+
/** Default cadence for fire-and-forget liveness pulses (5 minutes). */
|
|
6
|
+
export const DEFAULT_PULSE_INTERVAL_MS = 5 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
/** The `type` discriminator carried by liveness pulse messages. */
|
|
9
|
+
export const HEARTBEAT_MESSAGE_TYPE = 'Heartbeat';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Lightweight status snapshot included in an enriched pulse so the client can
|
|
13
|
+
* correlate the pulse with a run record and surface a coarse "still running"
|
|
14
|
+
* signal. Read from the in-memory run entity — never a DB query.
|
|
15
|
+
*/
|
|
16
|
+
export interface PulseStatus {
|
|
17
|
+
/** Primary key of the persisted run record (AIAgentRun, TestRun, TestSuiteRun). */
|
|
18
|
+
runId?: string;
|
|
19
|
+
/** Current run status, e.g. 'Running'. */
|
|
20
|
+
status?: string;
|
|
21
|
+
/** Optional human-readable current step for UI display. */
|
|
22
|
+
currentStep?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Handle returned by {@link startLivenessPulse}; call `stop()` when the work settles. */
|
|
26
|
+
export interface LivenessPulseHandle {
|
|
27
|
+
stop(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LivenessPulseOptions {
|
|
31
|
+
/** PubSub engine used to publish on the shared push-status topic. */
|
|
32
|
+
pubSub: PubSubEngine;
|
|
33
|
+
/** Session the client is subscribed on (used by the subscription filter). */
|
|
34
|
+
sessionId: string;
|
|
35
|
+
/** Resolver label echoed in the message envelope (e.g. 'RunAIAgentResolver'). */
|
|
36
|
+
resolver: string;
|
|
37
|
+
/** Pulse cadence in ms. Defaults to {@link DEFAULT_PULSE_INTERVAL_MS}. */
|
|
38
|
+
intervalMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Optional cheap status reader invoked on each tick. Should read from an
|
|
41
|
+
* in-memory ref, not the database. Errors are swallowed so a transient read
|
|
42
|
+
* never kills the pulse loop.
|
|
43
|
+
*/
|
|
44
|
+
readStatus?: () => PulseStatus | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Publishes a periodic liveness pulse on {@link PUSH_STATUS_UPDATES_TOPIC} while a
|
|
49
|
+
* fire-and-forget background operation runs. Each pulse resets the client's idle
|
|
50
|
+
* timer, so a long-but-active operation never spuriously times out. The pulse
|
|
51
|
+
* stops when the caller invokes `stop()` (typically in the background promise's
|
|
52
|
+
* `finally` block).
|
|
53
|
+
*
|
|
54
|
+
* The envelope matches the resolvers' existing push-status messages
|
|
55
|
+
* (`{ message: JSON.stringify({ resolver, type, status, data }), sessionId }`),
|
|
56
|
+
* so the client receives it through the same subscription with no special parsing.
|
|
57
|
+
*/
|
|
58
|
+
export function startLivenessPulse(options: LivenessPulseOptions): LivenessPulseHandle {
|
|
59
|
+
const { pubSub, sessionId, resolver, readStatus } = options;
|
|
60
|
+
const intervalMs = options.intervalMs ?? DEFAULT_PULSE_INTERVAL_MS;
|
|
61
|
+
|
|
62
|
+
const timer = setInterval(() => {
|
|
63
|
+
let data: PulseStatus | undefined;
|
|
64
|
+
try {
|
|
65
|
+
data = readStatus?.();
|
|
66
|
+
} catch (e) {
|
|
67
|
+
// A status read failure must not break the liveness loop.
|
|
68
|
+
LogError(`[LivenessPulse:${resolver}] readStatus failed: ${(e as Error).message}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
|
|
72
|
+
message: JSON.stringify({
|
|
73
|
+
resolver,
|
|
74
|
+
type: HEARTBEAT_MESSAGE_TYPE,
|
|
75
|
+
status: 'ok',
|
|
76
|
+
data: data ?? {},
|
|
77
|
+
}),
|
|
78
|
+
sessionId,
|
|
79
|
+
});
|
|
80
|
+
}, intervalMs);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
stop: () => clearInterval(timer),
|
|
84
|
+
};
|
|
85
|
+
}
|