Skip to content

fix(store): pass task object directly to setTaskCallback/removeTaskCallback to prevent duplicate event listeners - #723

Open
brain-frog wants to merge 8 commits into
webex:nextfrom
brain-frog:CAI-8283
Open

fix(store): pass task object directly to setTaskCallback/removeTaskCallback to prevent duplicate event listeners#723
brain-frog wants to merge 8 commits into
webex:nextfrom
brain-frog:CAI-8283

Conversation

@brain-frog

@brain-frog brain-frog commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

COMPLETES # https://jira-eng-sjc12.cisco.com/jira/browse/CAI-8283

https://jira-eng-sjc12.cisco.com/jira/browse/CAI-8283

This pull request addresses

Getting duplicate task acceptances
< DESCRIBE THE CONTEXT OF THE ISSUE >

by making the following changes

< DESCRIBE YOUR CHANGES >

  • Changed setTaskCallback and removeTaskCallback signatures to accept ITask instead of taskId string, eliminating volatile store.taskList lookup that caused orphaned listeners during React 18 StrictMode mount/unmount cycles
  • Added diagnostic logging with optional chaining on logger
  • Updated useIncomingTask and useCallControl hooks to pass task objects
  • Fixed leaking spy in useIncomingTask test (mockRestore)
  • Added jest.restoreAllMocks() in useCallControl beforeEach
  • Added regression test for removal when task absent from store.taskList
image onAccepted only invoked once

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Tooling change
  • Internal code refactor

The following scenarios were tested

  • The testing is done with the amplify link
    < ENUMERATE TESTS PERFORMED, WHETHER MANUAL OR AUTOMATED >

The GAI Coding Policy And Copyright Annotation Best Practices

  • GAI was not used (or, no additional notation is required)
  • Code was generated entirely by GAI
  • GAI was used to create a draft that was subsequently customized or modified
  • Coder created a draft manually that was non-substantively modified by GAI (e.g., refactoring was performed by GAI on manually written code)
  • Tool used for AI assistance (GitHub Copilot / Other - specify)
    • Github Copilot
    • Other - Claude
  • This PR is related to
    • Feature
    • Defect fix
    • Tech Debt
    • Automation

Checklist before merging

  • I have not skipped any automated checks
  • All existing and new tests passed
  • I have updated the testing document
  • I have tested the functionality with amplify link

Make sure to have followed the contributing guidelines before submitting.

…to prevent duplicate event listeners

- Changed setTaskCallback and removeTaskCallback signatures to accept ITask
  instead of taskId string, eliminating volatile store.taskList lookup that
  caused orphaned listeners during React 18 StrictMode mount/unmount cycles
- Added diagnostic logging with optional chaining on logger
- Updated useIncomingTask and useCallControl hooks to pass task objects
- Fixed leaking spy in useIncomingTask test (mockRestore)
- Added jest.restoreAllMocks() in useCallControl beforeEach
- Added regression test for removal when task absent from store.taskList

CAI-8283
@brain-frog
brain-frog requested a review from a team as a code owner July 21, 2026 20:16

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 340bf53809

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (!callback) return;
const task = this.store.taskList[taskId];
if (!task) return;
setTaskCallback = (event: TASK_EVENTS, callback, task: ITask) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve task-id callback callers

Existing @webex/cc-store consumers can still call setTaskCallback(event, cb, interactionId) because this singleton is a published/default package surface, and this change now treats that string as an ITask: the guard passes, the log prints an undefined id, and the call then fails at task.on(...) instead of registering the listener. Since this PR is described as non-breaking and only updates the in-repo callers, keep accepting the old string-id form (or add a separate new API) so already-published widgets/hosts do not crash after upgrading; the same compatibility issue applies to removeTaskCallback.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a36d733. setTaskCallback/removeTaskCallback are now (event, callback, taskId: string, task?: ITask). The required taskId restores the original string-based call path — if task is omitted, it resolves via store.taskList[taskId], so already-published external consumers still calling with just an interaction ID keep working. The optional task param lets in-repo callers (useIncomingTask, useCallControl) pass the task reference directly, which is what avoids the stale store.taskList lookup race this PR originally set out to fix. Added test coverage for both the task-object and taskId-only (legacy) call paths in store/tests/storeEventsWrapper.ts

…ack signature change

- Added STORE-R-022 requirement for ITask-based signatures
- Updated test strategy with regression test coverage
- Updated task spec overview, data flow, pitfalls, and do's/don'ts
- Reflects CAI-8283 fix for duplicate event listeners

CAI-8283
@aws-amplify-us-east-2

Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-723.d1b38q61t1z947.amplifyapp.com

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d284c97064

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

store.setTaskCallback(TASK_EVENTS.TASK_WRAPPEDUP, wrapupCallCallback, interactionId);
store.setTaskCallback(TASK_EVENTS.TASK_RECORDING_PAUSED, pauseRecordingCallback, interactionId);
store.setTaskCallback(TASK_EVENTS.TASK_RECORDING_RESUMED, resumeRecordingCallback, interactionId);
store.setTaskCallback(TASK_EVENTS.TASK_HOLD, holdCallback, currentTask);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register CallControl callbacks on the SDK task

When CallControl is driven by store.currentTask, this passes the store-exposed task into task.on, but setCurrentTask stores a cloned object (Object.assign(Object.create(Object.getPrototypeOf(task)), task)) rather than the SDK task instance held in taskList. The SDK/store task events are registered and emitted on the original task objects, so active-call callbacks such as onHoldResume, onEnd, and recording toggles can stop firing when listeners are attached to the clone; capture the original task reference for registration and reuse that same reference for cleanup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked into this — store.currentTask is indeed a clone (Object.assign(Object.create(Object.getPrototypeOf(task)), task) in setCurrentTask), not the original instance from taskList. However, since Task extends EventEmitter and Node's EventEmitter keeps its listener bucket in an own property (_events), that shallow Object.assign copies the _events object by reference, so the clone and the original share the same underlying listener storage. I verified this with a standalone repro using Node's real EventEmitter: registering via the clone and emitting on the original (and removing via the clone) both work correctly today.

Because of that, we're leaving useCallControl as-is rather than adding a store.taskList[interactionId] resolution step, to avoid extra complexity for a case that isn't actually broken. That said, this does rely on an EventEmitter implementation detail rather than a documented contract — if the SDK's Task class ever changes how it stores listeners (e.g., private class fields, a different emitter), this would need revisiting. Flagging this as known fragility rather than fixing further for now

- Fix removeTaskCallback guard test to assert on mockTask.off instead
  of mockTask.on so the assertion actually catches a regression
- Verify setTaskCallback receives the correct task object (not just
  any function) in the useCallControl event-listener test
- Update react-patterns.md example to pass the ITask object instead
  of the stale interactionId string to setTaskCallback/removeTaskCallback

@cmullenx cmullenx left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve conflicts and the codex comments

Resolve conflicts from upstream/next in storeEventsWrapper.ts, task/helper.ts,
and their tests/specs, keeping this PR's task-object-based
setTaskCallback/removeTaskCallback signature (instead of upstream's
taskId+task fallback) while folding in upstream's unrelated feature work
(participant drop roster, wxApp mute sync, DTMF keypad, etc.).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c0dfa76f77

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread packages/contact-center/store/src/storeEventsWrapper.ts Outdated
Matthew Olker added 2 commits August 28, 2026 13:19
…Callback

Addresses outstanding Codex review comment on PR webex#723: external/already-
published @webex/cc-store consumers may still call
setTaskCallback/removeTaskCallback with a string interactionId. Restore
that as the required third argument, resolving to
task ?? store.taskList[taskId], and make the ITask reference an optional
fourth argument so in-repo callers (useIncomingTask, useCallControl) keep
passing the task object directly to avoid the stale taskList lookup race
this PR originally fixed.

Update store/task tests and specs to cover both the task-object and
legacy taskId-only call paths.
…k logs

Task/interaction data is classified as agent/customer PII in
ai-docs/SECURITY.md and AGENTS.md rule 8 prohibits logging PII. Match
the rest of storeEventsWrapper.ts (e.g. removeCCCallback), which keeps
log messages free of payload values and carries only {module, method}
context.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants