@memberjunction/server 6.2.0-edge.1 → 6.2.0-edge.3
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 +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +2189 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -23
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +2143 -205
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +24441 -14433
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +15 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +16 -2
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
- package/dist/generic/PushStatusResolver.d.ts +38 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +84 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +56 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +191 -74
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +204 -50
- package/dist/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +77 -167
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.d.ts +55 -0
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
- package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +324 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts +7 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +16 -2
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +45 -89
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +94 -240
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +0 -16
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +23 -153
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
- package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
- package/dist/resolvers/RunDecisionResolver.js +396 -0
- package/dist/resolvers/RunDecisionResolver.js.map +1 -0
- package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -10
- package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +418 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +95 -92
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/AgentRunStatusPublisher.test.ts +175 -0
- package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
- package/src/__tests__/MeetingResolver.test.ts +374 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
- package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +182 -12
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
- package/src/__tests__/RunDecisionResolver.test.ts +699 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +43 -0
- package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/conversationRunEvents.test.ts +399 -0
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/pushStatusFanOut.test.ts +206 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/auth/__tests__/backward-compatibility.test.ts +1 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +24183 -17182
- package/src/generic/FireAndForgetHeartbeat.ts +16 -2
- package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
- package/src/generic/PushStatusResolver.ts +97 -1
- package/src/generic/ResolverBase.ts +220 -76
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +228 -51
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
- package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
- package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/MergeRecordsResolver.ts +14 -2
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +99 -287
- package/src/resolvers/RunAIAgentResolver.ts +28 -174
- package/src/resolvers/RunDecisionResolver.ts +435 -0
- package/src/resolvers/VectorizeEntityResolver.ts +16 -11
- package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +535 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/types.ts +5 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { PubSubEngine } from 'type-graphql';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Default cadence for fire-and-forget liveness pulses (60 seconds).
|
|
4
|
+
*
|
|
5
|
+
* MATCHED PAIR — do not change alone. The client's inactivity window
|
|
6
|
+
* (`DEFAULT_IDLE_TIMEOUT_MS` in GraphQLDataProvider's fireAndForgetHelper) is sized at roughly
|
|
7
|
+
* 3x this value, so a healthy run always refreshes the timer well before it expires. Lengthening
|
|
8
|
+
* this without lengthening that window makes every quiet stretch of a long agent run look like a
|
|
9
|
+
* stall and trips reconciliation; shortening the window without shortening this does the same.
|
|
10
|
+
*
|
|
11
|
+
* WAS 5 MINUTES (MJ #4222). Paired with the old 12-minute window, a client whose transport had
|
|
12
|
+
* silently died waited 12 minutes to notice — on runs that typically finish in 20 seconds. The
|
|
13
|
+
* pulse is a few dozen bytes and exists only while an operation is actually running, so the
|
|
14
|
+
* added traffic is negligible: a 40-minute run now sends ~40 pulses instead of ~8, and the
|
|
15
|
+
* 20-second run that motivated the change still sends none.
|
|
16
|
+
*/
|
|
3
17
|
export declare const DEFAULT_PULSE_INTERVAL_MS: number;
|
|
4
18
|
/** The `type` discriminator carried by liveness pulse messages. */
|
|
5
19
|
export declare const HEARTBEAT_MESSAGE_TYPE = "Heartbeat";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FireAndForgetHeartbeat.d.ts","sourceRoot":"","sources":["../../src/generic/FireAndForgetHeartbeat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAI5C
|
|
1
|
+
{"version":3,"file":"FireAndForgetHeartbeat.d.ts","sourceRoot":"","sources":["../../src/generic/FireAndForgetHeartbeat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAI5C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,yBAAyB,QAAY,CAAC;AAEnD,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,cAAc,CAAC;AAElD;;;;GAIG;AACH,MAAM,WAAW,WAAW;IACxB,mFAAmF;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,0FAA0F;AAC1F,MAAM,WAAW,mBAAmB;IAChC,IAAI,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,oBAAoB;IACjC,qEAAqE;IACrE,MAAM,EAAE,YAAY,CAAC;IACrB,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;CAC9C;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,mBAAmB,CA4BrF;AAED,kDAAkD;AAClD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,oBAAoB,GAAG,mBAAmB,CAErF"}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { LogError } from '@memberjunction/core';
|
|
2
2
|
import { PublishStatusUpdate } from './PushStatusResolver.js';
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Default cadence for fire-and-forget liveness pulses (60 seconds).
|
|
5
|
+
*
|
|
6
|
+
* MATCHED PAIR — do not change alone. The client's inactivity window
|
|
7
|
+
* (`DEFAULT_IDLE_TIMEOUT_MS` in GraphQLDataProvider's fireAndForgetHelper) is sized at roughly
|
|
8
|
+
* 3x this value, so a healthy run always refreshes the timer well before it expires. Lengthening
|
|
9
|
+
* this without lengthening that window makes every quiet stretch of a long agent run look like a
|
|
10
|
+
* stall and trips reconciliation; shortening the window without shortening this does the same.
|
|
11
|
+
*
|
|
12
|
+
* WAS 5 MINUTES (MJ #4222). Paired with the old 12-minute window, a client whose transport had
|
|
13
|
+
* silently died waited 12 minutes to notice — on runs that typically finish in 20 seconds. The
|
|
14
|
+
* pulse is a few dozen bytes and exists only while an operation is actually running, so the
|
|
15
|
+
* added traffic is negligible: a 40-minute run now sends ~40 pulses instead of ~8, and the
|
|
16
|
+
* 20-second run that motivated the change still sends none.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_PULSE_INTERVAL_MS = 60 * 1000;
|
|
5
19
|
/** The `type` discriminator carried by liveness pulse messages. */
|
|
6
20
|
export const HEARTBEAT_MESSAGE_TYPE = 'Heartbeat';
|
|
7
21
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FireAndForgetHeartbeat.js","sourceRoot":"","sources":["../../src/generic/FireAndForgetHeartbeat.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D
|
|
1
|
+
{"version":3,"file":"FireAndForgetHeartbeat.js","sourceRoot":"","sources":["../../src/generic/FireAndForgetHeartbeat.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnD,mEAAmE;AACnE,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAAC;AA2ClD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAA6B;IAC5D,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IACzE,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,IAAI,yBAAyB,CAAC;IAEnE,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC3B,IAAI,IAA6B,CAAC;QAClC,IAAI,CAAC;YACD,IAAI,GAAG,UAAU,EAAE,EAAE,CAAC;QAC1B,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,0DAA0D;YAC1D,QAAQ,CAAC,kBAAkB,QAAQ,wBAAyB,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;QACvF,CAAC;QAED,mBAAmB,CAAC,MAAM,EAAE;YACxB,SAAS;YACT,WAAW;YACX,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC;gBACpB,QAAQ;gBACR,IAAI,EAAE,sBAAsB;gBAC5B,MAAM,EAAE,IAAI;gBACZ,IAAI,EAAE,IAAI,IAAI,EAAE;aACnB,CAAC;SACL,CAAC,CAAC;IACP,CAAC,EAAE,UAAU,CAAC,CAAC;IAEf,OAAO;QACH,IAAI,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC;KACnC,CAAC;AACN,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,kBAAkB,CAAC,OAA6B;IAC5D,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;AACvC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
2
|
+
/**
|
|
3
|
+
* Grace period after a run reaches a terminal state before its conversation detail is considered
|
|
4
|
+
* abandoned. The normal path closes the detail moments after the run finishes; this waits well past
|
|
5
|
+
* that so a reconcile can never race a completion that is already on its way.
|
|
6
|
+
*/
|
|
7
|
+
export declare const ORPHAN_DETAIL_GRACE_MS: number;
|
|
8
|
+
/**
|
|
9
|
+
* Close conversation details left `In-Progress` by a run that is already over (MJ #4222).
|
|
10
|
+
*
|
|
11
|
+
* WHY THIS EXISTS. `AgentRunner` closes the detail as the final step of a run, so a process that
|
|
12
|
+
* dies mid-run never reaches it. The agent-run watchdog repairs the RUN — force-failing it once the
|
|
13
|
+
* heartbeat goes stale — but nothing repairs the detail, and the detail is the row the chat renders
|
|
14
|
+
* from. The result is durable state that disagrees with itself: a terminal run beside a message
|
|
15
|
+
* that still says it is being generated, spinning forever for anyone who opens it.
|
|
16
|
+
*
|
|
17
|
+
* WHY NOT IN THE SWEEP PROC. `spSweepStaleAIAgentRuns` is a single set-based UPDATE, and extending
|
|
18
|
+
* it to the detail would be the obvious fix. It would also be the wrong one: `MJ: Conversation
|
|
19
|
+
* Details` declares `TrustServerCacheCompletely`, so the server's RunView cache relies on
|
|
20
|
+
* `BaseEntity` events for invalidation and a stored procedure fires none. The row would change
|
|
21
|
+
* while every instance kept serving `In-Progress` from cache — a worse failure than the one being
|
|
22
|
+
* fixed, because reconciliation itself would read the stale value back. Going through
|
|
23
|
+
* `BaseEntity.Save()` costs a per-row write and buys correct invalidation everywhere.
|
|
24
|
+
*
|
|
25
|
+
* Idempotent and safe to run concurrently on several instances: the write is a no-op once the
|
|
26
|
+
* status is terminal, and each pass re-reads state before deciding.
|
|
27
|
+
*
|
|
28
|
+
* @returns How many details were closed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ReconcileOrphanedConversationDetails(provider: IMetadataProvider, contextUser: UserInfo, graceMs?: number): Promise<number>;
|
|
31
|
+
//# sourceMappingURL=OrphanedConversationDetailReconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrphanedConversationDetailReconciler.d.ts","sourceRoot":"","sources":["../../src/generic/OrphanedConversationDetailReconciler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGxE;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAgB,CAAC;AAiBpD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,oCAAoC,CACtD,QAAQ,EAAE,iBAAiB,EAC3B,WAAW,EAAE,QAAQ,EACrB,OAAO,GAAE,MAA+B,GACzC,OAAO,CAAC,MAAM,CAAC,CAkGjB"}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { LogError, LogStatus, RunView } from '@memberjunction/core';
|
|
2
|
+
import { UUIDsEqual } from '@memberjunction/global';
|
|
3
|
+
import { UserCache } from '@memberjunction/sqlserver-dataprovider';
|
|
4
|
+
/**
|
|
5
|
+
* Grace period after a run reaches a terminal state before its conversation detail is considered
|
|
6
|
+
* abandoned. The normal path closes the detail moments after the run finishes; this waits well past
|
|
7
|
+
* that so a reconcile can never race a completion that is already on its way.
|
|
8
|
+
*/
|
|
9
|
+
export const ORPHAN_DETAIL_GRACE_MS = 2 * 60 * 1000;
|
|
10
|
+
/** Ceiling on details closed per pass, so a large backlog cannot stall startup. */
|
|
11
|
+
const MAX_DETAILS_PER_PASS = 200;
|
|
12
|
+
/**
|
|
13
|
+
* Agent-run statuses that mean the run succeeded. `AwaitingFeedback` is the normal end of a chat
|
|
14
|
+
* turn: the agent replied and the next move is the user's.
|
|
15
|
+
*/
|
|
16
|
+
const SUCCESSFUL_RUN_STATUSES = ['Completed', 'AwaitingFeedback'];
|
|
17
|
+
/**
|
|
18
|
+
* Agent-run statuses that mean execution is over, whatever the outcome. `Paused` is absent: the
|
|
19
|
+
* run is parked on a workflow that is still executing.
|
|
20
|
+
*/
|
|
21
|
+
const TERMINAL_RUN_STATUSES = [...SUCCESSFUL_RUN_STATUSES, 'Failed', 'Cancelled'];
|
|
22
|
+
/**
|
|
23
|
+
* Close conversation details left `In-Progress` by a run that is already over (MJ #4222).
|
|
24
|
+
*
|
|
25
|
+
* WHY THIS EXISTS. `AgentRunner` closes the detail as the final step of a run, so a process that
|
|
26
|
+
* dies mid-run never reaches it. The agent-run watchdog repairs the RUN — force-failing it once the
|
|
27
|
+
* heartbeat goes stale — but nothing repairs the detail, and the detail is the row the chat renders
|
|
28
|
+
* from. The result is durable state that disagrees with itself: a terminal run beside a message
|
|
29
|
+
* that still says it is being generated, spinning forever for anyone who opens it.
|
|
30
|
+
*
|
|
31
|
+
* WHY NOT IN THE SWEEP PROC. `spSweepStaleAIAgentRuns` is a single set-based UPDATE, and extending
|
|
32
|
+
* it to the detail would be the obvious fix. It would also be the wrong one: `MJ: Conversation
|
|
33
|
+
* Details` declares `TrustServerCacheCompletely`, so the server's RunView cache relies on
|
|
34
|
+
* `BaseEntity` events for invalidation and a stored procedure fires none. The row would change
|
|
35
|
+
* while every instance kept serving `In-Progress` from cache — a worse failure than the one being
|
|
36
|
+
* fixed, because reconciliation itself would read the stale value back. Going through
|
|
37
|
+
* `BaseEntity.Save()` costs a per-row write and buys correct invalidation everywhere.
|
|
38
|
+
*
|
|
39
|
+
* Idempotent and safe to run concurrently on several instances: the write is a no-op once the
|
|
40
|
+
* status is terminal, and each pass re-reads state before deciding.
|
|
41
|
+
*
|
|
42
|
+
* @returns How many details were closed.
|
|
43
|
+
*/
|
|
44
|
+
export async function ReconcileOrphanedConversationDetails(provider, contextUser, graceMs = ORPHAN_DETAIL_GRACE_MS) {
|
|
45
|
+
try {
|
|
46
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
47
|
+
const detailResult = await rv.RunView({
|
|
48
|
+
EntityName: 'MJ: Conversation Details',
|
|
49
|
+
ExtraFilter: `Status = 'In-Progress' AND Role = 'AI'${hasTerminalRunClause(provider)}`,
|
|
50
|
+
OrderBy: '__mj_CreatedAt ASC',
|
|
51
|
+
MaxRows: MAX_DETAILS_PER_PASS,
|
|
52
|
+
ResultType: 'entity_object',
|
|
53
|
+
}, contextUser);
|
|
54
|
+
if (!detailResult.Success || !detailResult.Results?.length) {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
const runsByDetail = await loadTerminalRuns(rv, detailResult.Results.map(d => d.ID), contextUser);
|
|
58
|
+
if (runsByDetail.size === 0) {
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
const ownersByConversation = await loadConversationOwners(rv, detailResult.Results.map(d => d.ConversationID).filter((id) => !!id), contextUser);
|
|
62
|
+
const cutoff = Date.now() - graceMs;
|
|
63
|
+
let closed = 0;
|
|
64
|
+
for (const detail of detailResult.Results) {
|
|
65
|
+
const run = runsByDetail.get(detail.ID);
|
|
66
|
+
if (!run) {
|
|
67
|
+
// No run yet, or its run is still executing. Both are legitimately in progress.
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const completedAt = run.CompletedAt ? new Date(run.CompletedAt).getTime() : NaN;
|
|
71
|
+
if (!Number.isFinite(completedAt) || completedAt > cutoff) {
|
|
72
|
+
// Inside the grace window — the owning process may still be closing this itself.
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// Write as the conversation's OWNER, not as the system user.
|
|
76
|
+
//
|
|
77
|
+
// `MJConversationDetailEntityExtended.Save()` gates writes on conversation ownership: a
|
|
78
|
+
// non-owner without a resource grant is refused, returning false with no result
|
|
79
|
+
// registered anywhere — indistinguishable from a crash, and the reason this pass
|
|
80
|
+
// silently wrote nothing at all until it was traced. The system user owns no
|
|
81
|
+
// conversations, so it is exactly the caller that gate exists to stop. Saving as the
|
|
82
|
+
// owner satisfies the gate on its own terms rather than routing around it, and keeps
|
|
83
|
+
// the write attributable to the person whose message it is.
|
|
84
|
+
const owner = detail.ConversationID ? ownersByConversation.get(detail.ConversationID) : undefined;
|
|
85
|
+
if (!owner) {
|
|
86
|
+
LogError(`[OrphanDetailReconciler] No owner resolved for conversation ${detail.ConversationID}; leaving detail ${detail.ID}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const writable = await provider.GetEntityObject('MJ: Conversation Details', owner);
|
|
90
|
+
if (!(await writable.Load(detail.ID))) {
|
|
91
|
+
LogError(`[OrphanDetailReconciler] Could not load detail ${detail.ID} for write`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (writable.Status !== 'In-Progress') {
|
|
95
|
+
// Closed by something else since the list query. Its outcome stands.
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// The failure marker belongs only to the error branch. A completed run has nothing to
|
|
99
|
+
// report, so an empty message stays empty rather than contradicting its own status.
|
|
100
|
+
if (SUCCESSFUL_RUN_STATUSES.includes(run.Status)) {
|
|
101
|
+
writable.Status = 'Complete';
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
writable.Status = 'Error';
|
|
105
|
+
if (!writable.Message) {
|
|
106
|
+
writable.Message = run.ErrorMessage ?? '❌ Failed';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (await writable.Save()) {
|
|
110
|
+
closed++;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const result = writable.LatestResult;
|
|
114
|
+
LogError(`[OrphanDetailReconciler] Failed to close detail ${detail.ID}: ` +
|
|
115
|
+
`${result?.CompleteMessage || result?.Message || 'no message'}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (closed > 0) {
|
|
119
|
+
LogStatus(`[OrphanDetailReconciler] Closed ${closed} conversation detail(s) left behind by finished runs`);
|
|
120
|
+
}
|
|
121
|
+
return closed;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
// Maintenance work must never take the server down with it.
|
|
125
|
+
LogError(`[OrphanDetailReconciler] Pass failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function isQuotingProvider(provider) {
|
|
130
|
+
const p = provider;
|
|
131
|
+
return typeof p.QuoteIdentifier === 'function' && typeof p.QuoteSchemaAndView === 'function';
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Filter clause that keeps only details with at least one finished run.
|
|
135
|
+
*
|
|
136
|
+
* Details with no run, or whose run is still executing, are never closed here. Without this clause
|
|
137
|
+
* they fill the oldest-first window, and once 200 of them accumulate no closable orphan is reached.
|
|
138
|
+
* The newest-run check in {@link loadTerminalRuns} still decides each remaining detail.
|
|
139
|
+
*
|
|
140
|
+
* @returns An ` AND ...` clause, or an empty string when the provider cannot quote names.
|
|
141
|
+
*/
|
|
142
|
+
function hasTerminalRunClause(provider) {
|
|
143
|
+
const runs = provider.EntityByName('MJ: AI Agent Runs');
|
|
144
|
+
if (!runs || !isQuotingProvider(provider)) {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
const statuses = TERMINAL_RUN_STATUSES.map(s => `'${s}'`).join(',');
|
|
148
|
+
return (` AND ID IN (SELECT ${provider.QuoteIdentifier('ConversationDetailID')}` +
|
|
149
|
+
` FROM ${provider.QuoteSchemaAndView(runs.SchemaName, runs.BaseView)}` +
|
|
150
|
+
` WHERE ${provider.QuoteIdentifier('Status')} IN (${statuses}))`);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Newest terminal run per conversation detail. Details whose newest run is still executing are
|
|
154
|
+
* deliberately absent, so a retried message is judged on its latest attempt rather than an older
|
|
155
|
+
* finished one.
|
|
156
|
+
*/
|
|
157
|
+
async function loadTerminalRuns(rv, detailIds, contextUser) {
|
|
158
|
+
const quoted = detailIds.map(id => `'${id.replace(/'/g, "''")}'`).join(',');
|
|
159
|
+
const result = await rv.RunView({
|
|
160
|
+
EntityName: 'MJ: AI Agent Runs',
|
|
161
|
+
ExtraFilter: `ConversationDetailID IN (${quoted})`,
|
|
162
|
+
OrderBy: '__mj_CreatedAt DESC',
|
|
163
|
+
ResultType: 'entity_object',
|
|
164
|
+
}, contextUser);
|
|
165
|
+
if (!result.Success) {
|
|
166
|
+
return new Map();
|
|
167
|
+
}
|
|
168
|
+
const newest = new Map();
|
|
169
|
+
for (const run of result.Results ?? []) {
|
|
170
|
+
const detailId = run.ConversationDetailID;
|
|
171
|
+
if (!detailId || newest.has(detailId)) {
|
|
172
|
+
continue; // DESC order puts the newest run first
|
|
173
|
+
}
|
|
174
|
+
newest.set(detailId, run);
|
|
175
|
+
}
|
|
176
|
+
// Drop any detail whose newest attempt is still running.
|
|
177
|
+
for (const [detailId, run] of newest) {
|
|
178
|
+
if (!TERMINAL_RUN_STATUSES.includes(run.Status)) {
|
|
179
|
+
newest.delete(detailId);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return newest;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Owner `UserInfo` per conversation, resolved through {@link UserCache}. The reconciler writes as
|
|
186
|
+
* the owner so the conversation-detail permission gate is satisfied on its own terms.
|
|
187
|
+
*/
|
|
188
|
+
async function loadConversationOwners(rv, conversationIds, contextUser) {
|
|
189
|
+
const owners = new Map();
|
|
190
|
+
if (conversationIds.length === 0) {
|
|
191
|
+
return owners;
|
|
192
|
+
}
|
|
193
|
+
const quoted = [...new Set(conversationIds)].map(id => `'${id.replace(/'/g, "''")}'`).join(',');
|
|
194
|
+
const result = await rv.RunView({
|
|
195
|
+
EntityName: 'MJ: Conversations',
|
|
196
|
+
ExtraFilter: `ID IN (${quoted})`,
|
|
197
|
+
Fields: ['ID', 'UserID'],
|
|
198
|
+
ResultType: 'simple',
|
|
199
|
+
}, contextUser);
|
|
200
|
+
if (!result.Success) {
|
|
201
|
+
return owners;
|
|
202
|
+
}
|
|
203
|
+
const users = UserCache.Instance.Users;
|
|
204
|
+
for (const row of result.Results ?? []) {
|
|
205
|
+
if (!row.UserID) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const user = users.find(u => UUIDsEqual(u.ID, row.UserID));
|
|
209
|
+
if (user) {
|
|
210
|
+
owners.set(row.ID, user);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return owners;
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=OrphanedConversationDetailReconciler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"OrphanedConversationDetailReconciler.js","sourceRoot":"","sources":["../../src/generic/OrphanedConversationDetailReconciler.ts"],"names":[],"mappings":"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"}
|
|
@@ -22,7 +22,45 @@ export interface PushStatusNotificationPayload {
|
|
|
22
22
|
sessionId: string;
|
|
23
23
|
/** Authenticated user the update belongs to. Server-side filter key; never sent to the client. */
|
|
24
24
|
ownerUserId: string;
|
|
25
|
+
/**
|
|
26
|
+
* `MJGlobal.ProcessUUID` of the instance that first published this update. Used only for
|
|
27
|
+
* cross-instance echo suppression: an instance that receives its own message back from the
|
|
28
|
+
* message bus must drop it, or every push is delivered twice.
|
|
29
|
+
*/
|
|
30
|
+
SourceServerId?: string;
|
|
25
31
|
}
|
|
32
|
+
/** Receives every locally-published status update, for fan-out to other server instances. */
|
|
33
|
+
export type PushStatusPublishHook = (payload: PushStatusNotificationPayload) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Register (or clear, with no arg) the cross-instance fan-out hook.
|
|
36
|
+
*
|
|
37
|
+
* Inversion of control, deliberately: this module must not know that Redis exists. The server
|
|
38
|
+
* registers the hook at startup only when a message bus is configured, so a single-instance
|
|
39
|
+
* deployment behaves exactly as before.
|
|
40
|
+
*/
|
|
41
|
+
export declare function SetPushStatusPublishHook(hook?: PushStatusPublishHook): void;
|
|
42
|
+
/**
|
|
43
|
+
* Validate an update received from the cross-instance message bus.
|
|
44
|
+
*
|
|
45
|
+
* The bus carries messages from other processes, so every field is type-checked before the update
|
|
46
|
+
* is republished onto the local topic, where {@link StatusUpdatesFilter} compares `ownerUserId`
|
|
47
|
+
* with the connection's user.
|
|
48
|
+
*
|
|
49
|
+
* @param raw The JSON text taken off the bus.
|
|
50
|
+
* @param localServerId This process's `MJGlobal.ProcessUUID`, used to drop our own echo.
|
|
51
|
+
* @returns The update to republish, or `null` to drop it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ParseReplicatedStatusUpdate(raw: string, localServerId: string): PushStatusNotificationPayload | null;
|
|
54
|
+
/**
|
|
55
|
+
* Whether an update is worth sending to other server instances.
|
|
56
|
+
*
|
|
57
|
+
* Streaming content is excluded. It arrives at hundreds of messages per second and each one carries
|
|
58
|
+
* a full serialized agent run, so replicating it would cost far more bandwidth than the delivery it
|
|
59
|
+
* buys — and a token delta is worthless to a client that has already missed the ones before it.
|
|
60
|
+
* Progress and completion are low-rate and each is individually meaningful, which is exactly the
|
|
61
|
+
* kind of message that must survive landing on the wrong replica.
|
|
62
|
+
*/
|
|
63
|
+
export declare function ShouldReplicateStatusUpdate(message?: string): boolean;
|
|
26
64
|
interface PushStatusNotificationArgs {
|
|
27
65
|
sessionId: string;
|
|
28
66
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PushStatusResolver.d.ts","sourceRoot":"","sources":["../../src/generic/PushStatusResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,YAAY,EAAoD,MAAM,cAAc,CAAC;AAE1H,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,eAAO,MAAM,yBAAyB,wBAAwB,CAAC;AAE/D,qBACa,sBAAsB;IAEjC,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,IAAI,EAAG,IAAI,CAAC;IAGZ,SAAS,EAAG,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,6BAA6B;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,kGAAkG;IAClG,WAAW,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"PushStatusResolver.d.ts","sourceRoot":"","sources":["../../src/generic/PushStatusResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,YAAY,EAAoD,MAAM,cAAc,CAAC;AAE1H,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,eAAO,MAAM,yBAAyB,wBAAwB,CAAC;AAE/D,qBACa,sBAAsB;IAEjC,OAAO,CAAC,EAAE,MAAM,CAAC;IAGjB,IAAI,EAAG,IAAI,CAAC;IAGZ,SAAS,EAAG,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,6BAA6B;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,kGAAkG;IAClG,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,6FAA6F;AAC7F,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,6BAA6B,KAAK,IAAI,CAAC;AAIrF;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAE3E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,6BAA6B,GAAG,IAAI,CAyBpH;AAED;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAYrE;AAED,UAAU,0BAA0B;IAClC,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,mGAAmG;AACnG,MAAM,WAAW,kBAAkB;IACjC,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,WAAW,EAAE,MAAM,CAAC;IACpB,mGAAmG;IACnG,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAoB1F;AAED,mDAAmD;AACnD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAE1F;AAED,mFAAmF;AACnF,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,OAAO,EAAE,6BAA6B,CAAC;IACvC,IAAI,EAAE,0BAA0B,CAAC;IACjC,OAAO,EAAE,0BAA0B,GAAG,SAAS,CAAC;CACjD,GAAG,OAAO,CAUV;AAED,mDAAmD;AACnD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,OAAO,EAAE,6BAA6B,CAAC;IACvC,IAAI,EAAE,0BAA0B,CAAC;IACjC,OAAO,EAAE,0BAA0B,GAAG,SAAS,CAAC;CACjD,GAAG,OAAO,CAEV;AAED,qBACa,kBAAkB;IAM7B,aAAa,CAAG,mIAAmI;IACzI,EAAE,OAAO,EAAE,EAAE,6BAA6B,EAClB,SAAS,EAAE,MAAM,GAChD,sBAAsB;CAI1B"}
|
|
@@ -11,7 +11,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
13
|
import { Arg, Field, ID, ObjectType, Resolver, Root, Subscription } from 'type-graphql';
|
|
14
|
-
import { UUIDsEqual } from '@memberjunction/global';
|
|
14
|
+
import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
|
|
15
15
|
export const PUSH_STATUS_UPDATES_TOPIC = 'PUSH_STATUS_UPDATES';
|
|
16
16
|
let PushStatusNotification = class PushStatusNotification {
|
|
17
17
|
};
|
|
@@ -31,6 +31,76 @@ PushStatusNotification = __decorate([
|
|
|
31
31
|
ObjectType()
|
|
32
32
|
], PushStatusNotification);
|
|
33
33
|
export { PushStatusNotification };
|
|
34
|
+
let _publishHook;
|
|
35
|
+
/**
|
|
36
|
+
* Register (or clear, with no arg) the cross-instance fan-out hook.
|
|
37
|
+
*
|
|
38
|
+
* Inversion of control, deliberately: this module must not know that Redis exists. The server
|
|
39
|
+
* registers the hook at startup only when a message bus is configured, so a single-instance
|
|
40
|
+
* deployment behaves exactly as before.
|
|
41
|
+
*/
|
|
42
|
+
export function SetPushStatusPublishHook(hook) {
|
|
43
|
+
_publishHook = hook;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate an update received from the cross-instance message bus.
|
|
47
|
+
*
|
|
48
|
+
* The bus carries messages from other processes, so every field is type-checked before the update
|
|
49
|
+
* is republished onto the local topic, where {@link StatusUpdatesFilter} compares `ownerUserId`
|
|
50
|
+
* with the connection's user.
|
|
51
|
+
*
|
|
52
|
+
* @param raw The JSON text taken off the bus.
|
|
53
|
+
* @param localServerId This process's `MJGlobal.ProcessUUID`, used to drop our own echo.
|
|
54
|
+
* @returns The update to republish, or `null` to drop it.
|
|
55
|
+
*/
|
|
56
|
+
export function ParseReplicatedStatusUpdate(raw, localServerId) {
|
|
57
|
+
let parsed;
|
|
58
|
+
try {
|
|
59
|
+
parsed = JSON.parse(raw);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const { sessionId, ownerUserId, message, SourceServerId } = parsed;
|
|
68
|
+
const source = typeof SourceServerId === 'string' ? SourceServerId : undefined;
|
|
69
|
+
const body = typeof message === 'string' ? message : undefined;
|
|
70
|
+
if ((SourceServerId != null && source === undefined) || (message != null && body === undefined)) {
|
|
71
|
+
return null; // a present field of the wrong type
|
|
72
|
+
}
|
|
73
|
+
if (source === localServerId) {
|
|
74
|
+
return null; // our own message, echoed back
|
|
75
|
+
}
|
|
76
|
+
if (typeof sessionId !== 'string' || !sessionId || typeof ownerUserId !== 'string' || !ownerUserId) {
|
|
77
|
+
return null; // fail closed: an update with no valid identity can never be routed safely
|
|
78
|
+
}
|
|
79
|
+
return { sessionId, ownerUserId, message: body, SourceServerId: source };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether an update is worth sending to other server instances.
|
|
83
|
+
*
|
|
84
|
+
* Streaming content is excluded. It arrives at hundreds of messages per second and each one carries
|
|
85
|
+
* a full serialized agent run, so replicating it would cost far more bandwidth than the delivery it
|
|
86
|
+
* buys — and a token delta is worthless to a client that has already missed the ones before it.
|
|
87
|
+
* Progress and completion are low-rate and each is individually meaningful, which is exactly the
|
|
88
|
+
* kind of message that must survive landing on the wrong replica.
|
|
89
|
+
*/
|
|
90
|
+
export function ShouldReplicateStatusUpdate(message) {
|
|
91
|
+
if (!message) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(message);
|
|
96
|
+
const type = parsed?.type;
|
|
97
|
+
return type !== 'StreamingContent';
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// A plain-string message (not the resolvers' JSON envelope) is rare and low-rate. Replicate it.
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
34
104
|
/**
|
|
35
105
|
* The single, centralized vehicle for publishing on {@link PUSH_STATUS_UPDATES_TOPIC}. Every
|
|
36
106
|
* publisher — resolver or service — routes through here (resolvers via the
|
|
@@ -43,8 +113,21 @@ export function PublishStatusUpdate(pubSub, params) {
|
|
|
43
113
|
sessionId: params.sessionId,
|
|
44
114
|
ownerUserId: params.ownerUserId,
|
|
45
115
|
message: params.message,
|
|
116
|
+
SourceServerId: MJGlobal.Instance.ProcessUUID,
|
|
46
117
|
};
|
|
47
118
|
pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, payload);
|
|
119
|
+
// Fan out to other instances. Only reached by updates originating HERE: a message arriving from
|
|
120
|
+
// the bus is republished straight onto the local topic and never comes back through this
|
|
121
|
+
// function, so there is no loop to break beyond the SourceServerId check on the receiving side.
|
|
122
|
+
if (_publishHook && ShouldReplicateStatusUpdate(params.message)) {
|
|
123
|
+
try {
|
|
124
|
+
_publishHook(payload);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Fan-out is an optimization over the durable tail query. A message bus that is down must
|
|
128
|
+
// never break local delivery, which is the path that works for the common case.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
48
131
|
}
|
|
49
132
|
/** @deprecated Use {@link PublishStatusUpdate}. */
|
|
50
133
|
export function publishStatusUpdate(pubSub, params) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PushStatusResolver.js","sourceRoot":"","sources":["../../src/generic/PushStatusResolver.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,UAAU,EAAgB,QAAQ,EAAsB,IAAI,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC1H,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"PushStatusResolver.js","sourceRoot":"","sources":["../../src/generic/PushStatusResolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateExpression, BaseEntity, CompositeKey, DatabaseProviderBase, EntityInfo, EntityPermissionType, IMetadataProvider, RecordChangeFieldSecurityProjector, RunViewResult, UserInfo } from '@memberjunction/core';
|
|
1
|
+
import { AggregateExpression, BaseEntity, CompositeKey, DatabaseProviderBase, EntityFieldInfo, EntityInfo, EntityPermissionType, IMetadataProvider, RecordChangeFieldSecurityProjector, RunViewResult, UserInfo } from '@memberjunction/core';
|
|
2
2
|
import { MJUserViewEntityExtended } from '@memberjunction/core-entities';
|
|
3
3
|
import { PubSubEngine } from 'type-graphql';
|
|
4
4
|
import { RunViewGenericParams, UserPayload } from '../types.js';
|
|
@@ -169,7 +169,24 @@ export declare class ResolverBase {
|
|
|
169
169
|
* into the view. Subqueries against another entity's BaseView therefore do not inherit that
|
|
170
170
|
* entity's RLS; they are still restricted to the view (not the table).
|
|
171
171
|
*/
|
|
172
|
-
protected assertClientClauseUsesEntityBaseViews(clause: string | undefined | null, label: string, provider?: IMetadataProvider): void;
|
|
172
|
+
protected assertClientClauseUsesEntityBaseViews(clause: string | undefined | null, label: string, provider?: IMetadataProvider, user?: UserInfo): void;
|
|
173
|
+
/**
|
|
174
|
+
* SECURITY — screen for a FULL client-supplied SQL statement (ad-hoc query path). In addition
|
|
175
|
+
* to the SELECT-only validation the caller performs, every table reference must resolve to an
|
|
176
|
+
* entity BaseView the acting user can read UNSCOPED. CTE names defined by the statement itself
|
|
177
|
+
* are excluded from the check. Fails closed: no user, or an unresolvable reference, refuses.
|
|
178
|
+
*
|
|
179
|
+
* Entity CanRead is not unscoped base-view read authority: RunView narrows a granted read with
|
|
180
|
+
* row-level security and denied-field projection, and raw SQL applies neither. Rather than try
|
|
181
|
+
* to rewrite arbitrary SQL (joins, CTEs) to compose that policy, this refuses any entity that
|
|
182
|
+
* carries a row filter or a denied field for the caller — those reads belong on RunView. For
|
|
183
|
+
* the remaining entities CanRead genuinely means "every row and field of the view".
|
|
184
|
+
*/
|
|
185
|
+
protected assertFullQueryUsesReadableEntityViews(sqlText: string, provider: IMetadataProvider | undefined, user: UserInfo | undefined, label?: string): void;
|
|
186
|
+
/** Refuses an entity whose read is narrowed for this user by row-level security or field-level denials. */
|
|
187
|
+
private assertUnscopedRead;
|
|
188
|
+
/** Walks a parsed AST collecting the names of CTEs the statement itself defines. */
|
|
189
|
+
private collectCTENames;
|
|
173
190
|
/**
|
|
174
191
|
* Applies {@link assertClientClauseUsesEntityBaseViews} to every client-supplied clause
|
|
175
192
|
* a view request can carry that is actually a SQL fragment. GraphQL entry points
|
|
@@ -197,19 +214,42 @@ export declare class ResolverBase {
|
|
|
197
214
|
orderBy?: string | null;
|
|
198
215
|
userSearchString?: string | null;
|
|
199
216
|
overrideExcludeFilter?: string | null;
|
|
200
|
-
}, provider?: IMetadataProvider): void;
|
|
217
|
+
}, provider?: IMetadataProvider, user?: UserInfo): void;
|
|
201
218
|
/** Same write-node walk as EDS `sqlReadOnlyScreen.astContainsWriteNode`. */
|
|
202
219
|
private astContainsWriteNode;
|
|
203
220
|
private static readonly WRITE_NODE_TYPES;
|
|
204
221
|
private dialectForProvider;
|
|
222
|
+
/** True for a reference with no schema — which `SQLParser.ExtractTableRefs` reports as `dbo`. */
|
|
223
|
+
private isUnqualifiedSchema;
|
|
205
224
|
private stripSqlIdent;
|
|
225
|
+
/**
|
|
226
|
+
* `bare` maps an unqualified view name to its entity, or to `null` when the name exists in
|
|
227
|
+
* more than one schema — the database, not this screen, would decide which one runs.
|
|
228
|
+
*/
|
|
206
229
|
private entityBaseViewAllowList;
|
|
230
|
+
/**
|
|
231
|
+
* SECURITY — resolves a table reference against the entity BaseView allow-list and, when an
|
|
232
|
+
* acting user is supplied, additionally requires that user to hold CanRead on the referenced
|
|
233
|
+
* entity. Base views do not embed RLS and entity permissions are otherwise checked only on
|
|
234
|
+
* the TOP entity of a request, so without this check a subquery (or ad-hoc query) could read
|
|
235
|
+
* entities the caller has no read grant on.
|
|
236
|
+
*
|
|
237
|
+
* A schema-qualified reference must match that exact schema — `secret.vwFoo` never resolves
|
|
238
|
+
* through another schema's `vwFoo`. An unqualified reference resolves only when the view name
|
|
239
|
+
* is unique across schemas; an ambiguous one must be qualified, because the database's
|
|
240
|
+
* default-schema resolution, not this screen, would pick which view actually runs.
|
|
241
|
+
*
|
|
242
|
+
* `SQLParser.ExtractTableRefs` reports an unqualified reference as schema `dbo`, so `dbo` is
|
|
243
|
+
* indistinguishable from "no schema" here and both take the unqualified path (an exact
|
|
244
|
+
* `dbo.<view>` entity wins first).
|
|
245
|
+
*/
|
|
246
|
+
private assertTableRefReadable;
|
|
207
247
|
/**
|
|
208
248
|
* Optimized RunViewGenericInternal implementation with:
|
|
209
249
|
* - Field filtering at source (Fix #7)
|
|
210
250
|
* - Improved error handling (Fix #9)
|
|
211
251
|
*/
|
|
212
|
-
protected RunViewGenericInternal(provider: DatabaseProviderBase, viewInfo: MJUserViewEntityExtended, extraFilter: string, orderBy: string, userSearchString: string, excludeUserViewRunID: string | undefined, overrideExcludeFilter: string | undefined, saveViewResults: boolean | undefined, fields: string[] | undefined, ignoreMaxRows: boolean | undefined, excludeDataFromAllPriorViewRuns: boolean | undefined, forceAuditLog: boolean | undefined, auditLogDescription: string | undefined, resultType: string | undefined, userPayload: UserPayload | null, maxRows: number | undefined, startRow: number | undefined, aggregates?: AggregateExpression[], afterKey?: CompositeKey, bypassCache?: boolean, dataSource?: 'Live' | 'Materialized'): Promise<RunViewResult<any>>;
|
|
252
|
+
protected RunViewGenericInternal(provider: DatabaseProviderBase, viewInfo: MJUserViewEntityExtended, extraFilter: string, orderBy: string, userSearchString: string, excludeUserViewRunID: string | undefined, overrideExcludeFilter: string | undefined, saveViewResults: boolean | undefined, fields: string[] | undefined, ignoreMaxRows: boolean | undefined, excludeDataFromAllPriorViewRuns: boolean | undefined, forceAuditLog: boolean | undefined, auditLogDescription: string | undefined, resultType: string | undefined, userPayload: UserPayload | null, maxRows: number | undefined, startRow: number | undefined, aggregates?: AggregateExpression[], afterKey?: CompositeKey, bypassCache?: boolean, dataSource?: 'Live' | 'Materialized', includeBinaryFields?: boolean): Promise<RunViewResult<any>>;
|
|
213
253
|
/**
|
|
214
254
|
* Optimized implementation that:
|
|
215
255
|
* 1. Fetches user info only once (fixes N+1 query)
|
|
@@ -301,6 +341,11 @@ export declare class ResolverBase {
|
|
|
301
341
|
*
|
|
302
342
|
* Ordered so the boolean flag is evaluated last: the extra load lands only on entities that have
|
|
303
343
|
* the feature switched on, which is almost none of them.
|
|
344
|
+
*
|
|
345
|
+
* `MJ: Record Changes` always loads from the database: its server class allows an update only when
|
|
346
|
+
* Comments is the one dirty field, and "dirty" compared against client-supplied OldValues lets a
|
|
347
|
+
* caller pin forged audit columns as both old and new values. It doesn't track its own changes, so
|
|
348
|
+
* nothing else forces the load.
|
|
304
349
|
*/
|
|
305
350
|
protected MustLoadTruthFromDatabase(entityInfo: EntityInfo, input: {
|
|
306
351
|
OldValues___?: Array<{
|
|
@@ -362,6 +407,13 @@ export declare class ResolverBase {
|
|
|
362
407
|
Value: unknown;
|
|
363
408
|
}>;
|
|
364
409
|
} & Record<string, unknown>, clientNewValues: Record<string, unknown>, provider?: IMetadataProvider): boolean;
|
|
410
|
+
/**
|
|
411
|
+
* Converts one client-sent old value (always a string on the wire; dates as epoch milliseconds,
|
|
412
|
+
* the GraphQL Timestamp form) to the field's TypeScript type, so it compares equal to the stored
|
|
413
|
+
* value. Both the OldValues comparison and the load-from-OldValues path use it: without it a date
|
|
414
|
+
* old value becomes an Invalid Date and an unchanged date field reads as edited.
|
|
415
|
+
*/
|
|
416
|
+
protected ClientOldValueToFieldValue(field: EntityFieldInfo | undefined, raw: unknown): string | number | boolean | Date | null;
|
|
365
417
|
/**
|
|
366
418
|
* This routine compares the OldValues property in the input object to the values in the DB that we just loaded. If there are differences, we need to check to see if the client
|
|
367
419
|
* is trying to update any of those fields (e.g. overlap). If there is overlap, we throw an error. If there is no overlap, we can proceed with the update even if the DB Values
|