Start here → root
AGENTS.md(agent entry) · routerSPEC_INDEX.md· systemARCHITECTURE.md. This is the module's canonical spec: orientation, requirements, design, flows, state, and tests. Context-efficiency: link to canonical docs — don't duplicate them. Load specs on demand perSPEC_INDEX.md.
| Field | Value |
|---|---|
| Module id | store |
| Source path(s) | packages/contact-center/store/src/ |
| Doc kind | Module spec |
| Coverage score | Pending coverage assessment |
| Generated from | module-spec @ SDLC template library 0.1.0-draft |
| generated_by / approved_by / updated_at | generated_by: migration agent / approved_by: pending / updated_at: 2026-06-29 |
| Validation status | not-run |
Coverage score: Pending coverage assessment before the first report; after assessment, replace with
<0-100%> plus the report path/evidence. Keep manifest coverage state outside the rendered module doc
metadata.
Every generated requirement below must cite concrete source evidence using file path. Separate source
evidence, test evidence, examples, assumptions, and gaps so validators and future agents can distinguish
truth from context. Test evidence is preferred for WHY. Commit evidence is allowed only when the
repository policy says history is reliable, and must include the commit hash. If evidence is missing or
conflicting, ask a focused discovery question before finalizing the requirement; record unresolved answers
as approved unknowns only when the human explicitly defers or does not know.
| Source doc | Scope | Decision | Detail location or disposition |
|---|---|---|---|
ai-docs/_archive/pre-sdlc-migration/packages/contact-center/store/ai-docs/AGENTS.md |
overview / API / usage | migrated | Overview, Purpose, Public Surface, Use Cases; usage snippets condensed to behavior. |
ai-docs/_archive/pre-sdlc-migration/packages/contact-center/store/ai-docs/ARCHITECTURE.md |
architecture / sequence diagrams | reconciled | Design Overview, Data Flow, Sequence Diagram(s), Pitfalls. Diagrams re-derived from current store.ts / storeEventsWrapper.ts; see Conflicts note below for drift corrected. |
@webex/contact-center package types (node_modules/@webex/contact-center/dist/types/index.d.ts) |
SDK API reference (installed .d.ts) |
reference-only | Linked as the authoritative source for SDK-shaped types/methods consumed via store.cc.*. |
@webex/cc-store is the single shared MobX store for every Webex Contact Center widget. It is the sole boundary between widgets and the @webex/contact-center SDK: widgets never import the SDK directly — they read observables and call methods on the store, which proxies to store.cc.*. The package is structured in two layers. Store (src/store.ts) is a makeAutoObservable singleton (Store.getInstance()) that holds raw observable state and owns initialization/registration with the SDK. StoreWrapper (src/storeEventsWrapper.ts) is the default export — it wraps the singleton, getter-proxies every observable, owns all SDK event wiring (CC + task events), exposes mutators (all writes funnel through runInAction), list-fetch helpers, callback registration, and task-lifecycle handling.
src/index.ts re-exports the StoreWrapper instance as the default export plus everything from store.types.ts (types, the CC_EVENTS / TASK_EVENTS enums, login/consult/campaign constants) and task-utils.ts (pure selectors over SDK ITask objects). util.ts extracts a fixed allow-list of feature flags from the agent Profile at registration time.
A maintainer should start at src/store.ts to understand the observable shape and init/register flow, then src/storeEventsWrapper.ts for how SDK events drive observable updates, then src/task-utils.ts for the read-only task/consult/conference selectors widgets consume.
Owns Contact Center client-side state and the SDK boundary: initialize/register with @webex/contact-center, subscribe to CC and task events, expose reactive observables and mutators, fetch domain lists (buddy agents, queues, entry points, address book), and centralize the error callback. It does NOT own UI rendering, business validation, or any direct network protocol beyond delegating to the SDK.
TypeScript 5.6.3, MobX 6.13.5 (makeAutoObservable, observable.ref, runInAction). Consumed in React 18 via mobx-react-lite observer() in downstream packages (not a dependency of this package itself). SDK peer @webex/contact-center 3.12.0-next.82. Tests: Jest 29 + ts compile (tsc --project tsconfig.test.json && jest --coverage). Build target: dist/index.js (Webpack). Evidence: packages/contact-center/store/package.json.
packages/contact-center/store/src/
├── index.ts # Barrel: default StoreWrapper instance + re-export of types & task-utils
├── store.ts # Store singleton: MobX observables, init() + registerCC()
├── storeEventsWrapper.ts # StoreWrapper (default export): observable proxies, event wiring, mutators, list fetchers, task lifecycle
├── store.types.ts # Types/interfaces, CC_EVENTS & TASK_EVENTS enums, ConsultStatus, login/campaign constants
├── task-utils.ts # Pure selectors over ITask (incoming, consult status, conference participants, hold status)
├── util.ts # getFeatureFlags(): allow-list extraction from agent Profile
└── constants.ts # Task/interaction/consult state + participant-type string constants
Tests mirror src under packages/contact-center/store/tests/ (store.ts, storeEventsWrapper.ts, task-utils.ts, util.ts).
| File | Holds |
|---|---|
packages/contact-center/store/src/store.ts |
The observable state shape, the 6000ms init timeout, and the registerCC profile→observable mapping. Never re-declare these defaults elsewhere. |
packages/contact-center/store/src/store.types.ts |
CC_EVENTS / TASK_EVENTS event-name enums, ConsultStatus, LoginOptions order, ERROR_TRIGGERING_IDLE_CODES, CAMPAIGN_PREVIEW_* type lists, and the public export barrel. |
packages/contact-center/store/src/util.ts |
The exact feature-flag allow-list parsed from the agent profile. |
packages/contact-center/store/src/constants.ts |
Canonical task/interaction/consult state strings and EXCLUDED_PARTICIPANT_TYPES. |
packages/contact-center/store/src/index.ts |
The public export surface (default store + types + task-utils). |
This module is consumed as an imported SDK/code API (the @webex/cc-store package), not a network surface. Root index: CONTRACTS.md.
| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index |
|---|---|---|---|---|---|---|
store.instance |
SDK | default export store (StoreWrapper singleton); init(options, setupEventListeners), registerCC(webex?), observable getters, mutators, getBuddyAgents/getQueues/getEntryPoints/getAddressBookEntries, setOnError, setCCCallback/removeCCCallback, setTaskCallback/removeTaskCallback |
Sole SDK access point and shared reactive state for all CC widgets | stable semver; observable getter set is additive | packages/contact-center/store/src/storeEventsWrapper.ts, src/store.ts |
CONTRACTS.md |
store.types |
SDK | type re-exports (IContactCenter, ITask, Profile, Team, IStore, IStoreWrapper, InitParams, RealTimeTranscriptionData, ~20 more) |
Typed domain surface for widget code | stable semver; SDK-shaped types track the SDK | packages/contact-center/store/src/store.types.ts:334-366; SDK: @webex/contact-center types (node_modules/@webex/contact-center/dist/types/index.d.ts) |
CONTRACTS.md |
store.constants |
SDK | value/enum exports (CC_EVENTS, TASK_EVENTS, ConsultStatus, LoginOptions, CAMPAIGN_PREVIEW_*, DESKTOP/EXTENSION/DIAL_NUMBER) |
Event names + domain enums for widgets | stable semver | packages/contact-center/store/src/store.types.ts:368-403 |
CONTRACTS.md |
store.task-utils |
SDK | pure selectors (isIncomingTask, getTaskStatus, getConsultStatus, getConferenceParticipants, getConferenceParticipantsCount, isInteractionOnHold, findHoldStatus, findHoldTimestamp, etc.) |
Read-only derivations over ITask |
stable semver | packages/contact-center/store/src/task-utils.ts |
CONTRACTS.md |
Compatibility notes:
- Adding a new observable getter or mutator is additive (minor). Removing/renaming an observable, mutator, or changing the
CC_EVENTS/TASK_EVENTSenum values is breaking (major) — widgets and the SDK event stream depend on the exact string values. - The
CC_EVENTS/TASK_EVENTSenums are locally declared until the SDK exports them (see// TODO: remove this once cc sdk exports this enum,store.types.ts:247). They must stay byte-identical to the SDK's emitted event strings.
@webex/contact-centerSDK (peer, floor pinned inpackage.jsonat3.12.0-next.82) — the entire CC runtime:Webex.init(),webex.cc.*methods, the CC/task event stream, agentProfile,webex.credentials.getUserToken(). Consumed ONLY through the store. Fallback on unavailability:Store.init()rejects after a 6000ms timeout (src/store.ts:140-142); the wrapper wraps the rejection and invokesonErrorCallback('Store', err)(src/storeEventsWrapper.ts:442-452).mobx^6.13.5 — observable state andrunInActionfor all mutations.- Internal: none upstream. The store is the lowest widget-layer dependency (
cc-components → widget packages → store → SDK); it imports no widget package.
| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence |
|---|---|---|---|---|---|---|
STORE-R-001 |
Store.getInstance() returns one shared singleton instance; the default export is a single StoreWrapper over it |
All widgets must share one source of truth for agent/session/task state | packages/contact-center/store/src/store.ts:64-72, src/storeEventsWrapper.ts:51-53,1112-1114 |
tests/store.ts ("should initialize with default values") |
none | PRESENT |
STORE-R-002 |
init({webex}) registers immediately; init({webexConfig, access_token}) calls Webex.init(), waits for the ready event, then registers |
Supports both host-provided Webex and store-bootstrapped Webex | src/store.ts:132-188 |
tests/store.ts (init: "should call registerCC if webex is in options", "should initialize webex and call registerCC on ready event") |
none | PRESENT |
STORE-R-003 |
When bootstrapping Webex, init rejects with Webex SDK failed to initialize if the ready event has not fired within 6000ms |
Prevents widgets hanging forever on an unreachable SDK | src/store.ts:139-142 |
tests/store.ts ("should reject the promise if Webex SDK fails to initialize") |
none | PRESENT |
STORE-R-004 |
registerCC() throws Webex SDK not initialized when neither a webex arg nor a prior this.cc exists |
Fail fast on misuse instead of a later null deref | src/store.ts:74-81 |
tests/store.ts ("should throw error if webex and cc object are not present") |
none | PRESENT |
STORE-R-005 |
On successful register(), the profile is mapped into observables (teams, idleCodes, agentId, wrapupCodes, deviceType, dialNumber, teamId, timestamps, feature flags); registration failures reject and are logged |
Populates initial state so widgets render correctly; surfaces failures | src/store.ts:89-129 |
tests/store.ts ("should initialise store values on successful register", "should log an error on failed register") |
none | PRESENT |
STORE-R-006 |
loginOptions excludes BROWSER unless webRtcEnabled, and is sorted by the LoginOptions key order |
WebRTC/browser calling is gated by org capability; UI ordering must be stable | src/store.ts:100-103, src/store.types.ts:319-323 |
tests/store.ts ("should initialise store values on successful register") |
none | PRESENT |
STORE-R-007 |
featureFlags is restricted to a fixed allow-list of profile keys, omitting undefined values |
Avoid leaking arbitrary profile fields and keep a known flag surface | src/util.ts:3-36 |
tests/util.ts ("should return an object with feature flags from agent profile...") |
none | PRESENT |
STORE-R-008 |
All observable mutations go through runInAction (directly or via mutators) |
MobX strict-mode correctness; batched, atomic reactive updates | src/storeEventsWrapper.ts (e.g. 189-237, 269-282, 303-323, 906-921, 1008-1023) |
tests/storeEventsWrapper.ts ("storeEventsWrapper Proxies", "setState") |
none | PRESENT |
STORE-R-009 |
setCurrentTask ignores incoming tasks and pending (state new, not yet accepted) campaign-preview tasks (clears currentTask); deep-clones the task; fires onTaskSelected only when the task actually changes |
CallControl must not render for previews still showing Accept/Skip; avoid stale callbacks | src/storeEventsWrapper.ts:243-283 |
tests/storeEventsWrapper.ts ("setCurrentTask", "campaign preview task lifecycle") |
none | PRESENT |
STORE-R-010 |
refreshTaskList() re-reads cc.taskManager.getAllTasks() and reconciles currentTask: clears + resets state when empty, keeps current if still present, else promotes the first task |
Keep the store's task view consistent with the SDK after any task event | src/storeEventsWrapper.ts:303-323 |
tests/storeEventsWrapper.ts ("refreshTaskList") |
none | PRESENT |
STORE-R-011 |
Incoming tasks register the full task-event listener set once; the onIncomingTask callback fires only for genuinely new tasks (not already in taskList) |
Avoid duplicate listeners and duplicate incoming-task UI for consult/re-entry | src/storeEventsWrapper.ts:690-762 |
tests/storeEventsWrapper.ts ("storeEventsWrapper events reactions") |
none | PRESENT |
STORE-R-012 |
handleTaskRemove detaches every task listener, clears realtimeTranscriptionData for the removed current task, drops accepted-campaign tracking, resets custom state, and refreshes the list |
Prevent listener/audio/state leaks across task lifecycles | src/storeEventsWrapper.ts:458-521 |
tests/storeEventsWrapper.ts ("handleTaskRemove — campaign ID cleanup") |
Per-listener detach is asserted only partially; full leak audit is a gap | PRESENT |
STORE-R-013 |
agent:logoutSuccess triggers cleanUpStore() which resets session observables and removes CC SDK listeners; agent:multiLogin sets showMultipleLoginAlert |
Clean session teardown and multi-login warning | src/storeEventsWrapper.ts:811-819,1003-1024,1029-1066 |
tests/storeEventsWrapper.ts ("storeEventsWrapper events reactions") |
none | PRESENT |
STORE-R-014 |
agent:stateChange (type AgentStateChangeSuccess) updates currentState (defaulting auxCodeId ''→'0') and both state-change timestamps |
Drives the agent-state widget and timers | src/storeEventsWrapper.ts:797-809 |
tests/storeEventsWrapper.ts ("storeEventsWrapper events reactions") |
none | PRESENT |
STORE-R-015 |
List fetchers proxy the SDK and propagate errors after logging; getQueues filters by upper-cased channel type; getAddressBookEntries returns empty when isAddressBookEnabled is false |
Centralize SDK fetch + transform so widgets stay SDK-agnostic | src/storeEventsWrapper.ts:924-1001 |
tests/storeEventsWrapper.ts ("storeEventsWrapper", "getAccessToken") |
getBuddyAgents/getQueues happy-path filtering covered; address-book disabled branch coverage is a gap |
PRESENT |
STORE-R-016 |
setOnError wraps the caller callback to also submit a behavioral metrics event before invoking it |
Consistent telemetry on widget errors | src/storeEventsWrapper.ts:285-301 |
None found | Negative/telemetry-path test missing | WEAK |
STORE-R-017 |
isIncomingTask returns true only when the task is not wrap-up-required, the agent has not joined, and the interaction state is new/consult/connected/conference |
Gates whether a task is treated as an unanswered incoming offer | src/task-utils.ts:26-37 |
tests/task-utils.ts ("isIncomingTask" — incoming / not incoming / edge cases) |
none | PRESENT |
STORE-R-018 |
getConsultStatus/getTaskStatus map participant consultState + interaction state to a ConsultStatus, with special handling for secondary EP-DN agents |
Consult/conference UI relies on a single derived status | src/task-utils.ts:39-146 |
None found (direct getConsultStatus test) |
Only isIncomingTask, conference, and hold helpers are directly tested; consult-status helper is a gap |
WEAK |
STORE-R-019 |
Conference helpers (getIsConferenceInProgress, getConferenceParticipants, getConferenceParticipantsCount) count only active agent participants, excluding Customer/Supervisor/VVA and those who left |
Accurate conference participant display | src/task-utils.ts:148-247, src/constants.ts:33 |
tests/task-utils.ts ("getIsConferenceInProgress", "getConferenceParticipants", "getConferenceParticipantsCount") |
none | PRESENT |
STORE-R-020 |
findHoldTimestamp/findHoldStatus resolve hold state per media type, remapping to mainCall for secondary EP-DN agents |
Hold timers align with Agent Desktop across consult/conference | src/task-utils.ts:285-362 |
tests/task-utils.ts ("findHoldTimestamp") |
findHoldStatus direct coverage is a gap |
PRESENT |
STORE-R-021 |
handleRealtimeTranscription upserts transcript lines keyed by messageId, normalizing role/timestamp and dropping empty content |
Live transcription panel needs deduped, ordered lines | src/storeEventsWrapper.ts:891-922 |
None found | No dedicated transcription test located | WEAK |
STORE-R-022 |
setTaskCallback(event, callback, task: ITask) and removeTaskCallback(event, callback, task: ITask) accept the task object directly (not a string ID), call task.on()/task.off() on that reference, and guard on !callback || !task; diagnostic logging uses optional chaining on this.store.logger |
Eliminates the store.taskList[taskId] lookup race: if the task is removed from the list before the React effect cleanup fires, the old implementation silently skipped task.off(), orphaning listeners and causing duplicate SDK callbacks on the next task |
src/storeEventsWrapper.ts:417-427,453-463 |
tests/storeEventsWrapper.ts ("should set task callback", "should remove task callback", "should remove task callback even when task is absent from store.taskList") |
none | PRESENT |
The store is deliberately split into a thin observable core and a thick wrapper. Store (store.ts) holds only field declarations + makeAutoObservable (with cc as observable.ref so the SDK object itself is not deeply observed) and the two lifecycle methods init/registerCC. Everything reactive and event-driven lives in StoreWrapper (storeEventsWrapper.ts), which composes the singleton via Store.getInstance() and re-exposes each field through a getter. This keeps the observable schema in one place while concentrating SDK coupling, event wiring, and mutation discipline in the wrapper.
Initialization has two entry shapes (InitParams = WithWebex | WithWebexConfig). With a host-supplied webex, the wrapper wires event listeners and registers synchronously. Without one, the store calls Webex.init(), arms a 6000ms timeout, and waits for the ready event before wiring listeners and registering; the timeout guards against an SDK that never becomes ready. Registration maps the agent Profile into observables once.
Event handling is the heart of the wrapper. setupIncomingTaskHandler is passed into init and attaches CC-level listeners (stationLoginSuccess, dnRegistered/reloginSuccess, multiLogin, stateChange, logoutSuccess, task incoming/hydrate/merged/campaign-preview). Per-task listeners are attached in registerTaskEventListeners when a task arrives and symmetrically detached in handleTaskRemove. Most task events simply call refreshTaskList(), which re-reads the SDK's authoritative task map and reconciles currentTask. Campaign-preview tasks carry extra state logic (RESERVED vs ENGAGED, an acceptedCampaignIds set) so a pending preview never promotes to currentTask.
Mutations are funneled through small mutator methods that wrap runInAction, satisfying MobX strict mode and keeping reactive updates atomic. task-utils.ts is pure (no store state) — selectors that downstream widgets call to derive consult/conference/hold status from an ITask.
In-process MobX reactivity; the only external transport is the SDK event stream and method calls (@webex/contact-center), which is itself WebSocket/HTTP under the hood but opaque to this module.
graph TB
subgraph Host
App[Host app / cc-widgets]
end
subgraph Widgets
Hook[Custom hook]
UI[observer component]
end
subgraph State
Wrapper[StoreWrapper<br/>storeEventsWrapper.ts]
Store[Store singleton<br/>store.ts]
end
SDK["@webex/contact-center SDK"]
App -->|init / registerCC| Wrapper
Hook -->|reads observables / calls methods| Wrapper
Wrapper -->|getter proxy| Store
Wrapper -->|store.cc.* methods| SDK
SDK -->|CC + task events| Wrapper
Wrapper -->|runInAction mutations| Store
Store -->|observable reactions| Hook
Hook --> UI
Sequence coverage:
| Operation group | Diagram | Failure / recovery coverage |
|---|---|---|
| Init + register | "Store init / register" | 6000ms init timeout reject; register reject; wrapper error callback |
| SDK event → observable update | "Agent state change & multi-login" | non-AgentStateChangeSuccess payloads ignored |
| Incoming task lifecycle | "Incoming task → assigned → end/remove" | duplicate-task guard; campaign-preview RESERVED branch; listener detach on remove |
Representative store.cc.* call |
"getQueues list fetch" | SDK error logged + rethrown |
sequenceDiagram
participant App
participant W as StoreWrapper
participant S as Store
participant SDK as "@webex/contact-center"
App->>W: init(options)
W->>S: init(options, setupIncomingTaskHandler)
alt options has webex
S->>W: setupEventListeners(webex.cc)
S->>S: registerCC(webex)
else webexConfig + access_token
S->>S: setTimeout(6000ms)
S->>SDK: Webex.init({config, credentials})
alt ready within 6s
SDK-->>S: ready
S->>S: clearTimeout; setupEventListeners(webex.cc)
S->>S: registerCC(webex)
else timeout elapses
S-->>W: reject("Webex SDK failed to initialize")
end
end
S->>SDK: cc.register()
alt register resolves
SDK-->>S: Profile
S->>S: map profile → observables, getFeatureFlags()
S-->>W: resolve
W-->>App: resolve
else register rejects
SDK-->>S: error
S-->>W: reject(error)
W->>W: onErrorCallback("Store", err)
W-->>App: throw err
end
sequenceDiagram
participant SDK as "@webex/contact-center"
participant W as StoreWrapper
participant S as Store
SDK-->>W: agent:stateChange (data)
alt data.type == "AgentStateChangeSuccess"
W->>S: setCurrentState(auxCodeId || "0")
W->>S: setLastStateChangeTimestamp(ts)
W->>S: setLastIdleCodeChangeTimestamp(ts)
else other payload
W->>W: ignore
end
SDK-->>W: agent:multiLogin (AgentMultiLoginCloseSession)
W->>S: setShowMultipleLoginAlert(true)
SDK-->>W: agent:logoutSuccess
W->>W: setAgentProfile({}); cleanUpStore(); removeEventListeners()
sequenceDiagram
participant SDK as "@webex/contact-center"
participant W as StoreWrapper
participant S as Store
SDK-->>W: task:incoming (ITask)
W->>W: registerTaskEventListeners(task)
alt task not already in taskList
W->>W: onIncomingTask({task}); handleTaskMuteState(task)
end
W->>S: refreshTaskList()
SDK-->>W: task:assigned
alt campaign preview & state == "new"
W->>S: setState(RESERVED)
else
W->>S: setCurrentTask(task); setState(ENGAGED)
end
SDK-->>W: task:end
W->>S: setIsDeclineButtonEnabled(false); refreshTaskList()
Note over W,S: handleTaskRemove detaches all task listeners,<br/>clears transcription, drops accepted-campaign id,<br/>resets state, refreshTaskList()
sequenceDiagram
participant Widget
participant W as StoreWrapper
participant SDK as "@webex/contact-center"
Widget->>W: getQueues(mediaType, params)
W->>SDK: cc.getQueues(params)
alt resolves
SDK-->>W: queues
W->>W: filter by channelType == mediaType.toUpperCase()
W-->>Widget: {data, meta}
else rejects
SDK-->>W: error
W->>W: logger.error(...)
W-->>Widget: throw error
end
classDiagram
class IStore { <<interface>> }
class IStoreWrapper { <<interface>> }
class Store { -instance: Store; +getInstance(); +init(); +registerCC() }
class StoreWrapper { +store: IStore; +init(); +registerCC(); +refreshTaskList(); +get* observables }
IStoreWrapper --|> IStore
Store ..|> IStore
StoreWrapper ..|> IStoreWrapper
StoreWrapper o-- Store : composes (getInstance)
StoreWrapper ..> task_utils : uses isIncomingTask
StoreWrapper ..> SDK : store.cc.*
Store ..> SDK : Webex.init / cc.register
class task_utils { <<module>> isIncomingTask getConsultStatus getConferenceParticipants findHoldStatus }
StoreWrapper extends the IStore contract (via IStoreWrapper) and composes a single Store singleton, proxying every observable through getters. Store implements IStore and is the only class that touches Webex.init()/cc.register(). task-utils is a stateless module of selectors that the wrapper and downstream widgets call against ITask.
- UC-1 Bootstrap with host Webex: Host calls
store.init({webex})after the SDKreadyevent → wrapper wires listeners andregisterCCmaps the profile into observables → widgets render. Evidence:src/store.ts:132-138,tests/store.ts(init). - UC-2 Bootstrap Webex from store: Host calls
store.init({webexConfig, access_token})→ store runsWebex.init(), waits forready(or rejects at 6s), then registers. Evidence:src/store.ts:139-188,tests/store.ts(init). - UC-3 Observe agent/session state in React: Widget wraps in
observer()and readsstore.agentId,store.isAgentLoggedIn,store.deviceType,store.currentState→ re-renders on mutation. Evidence:src/storeEventsWrapper.ts:56-187,_archive/.../AGENTS.mdusage. - UC-4 Handle an incoming task through to wrap-up: SDK
task:incoming→ listeners registered +onIncomingTaskfired →task:assignedsets ENGAGED/current →task:end+handleTaskRemovecleans up. Evidence:src/storeEventsWrapper.ts:585-762,tests/storeEventsWrapper.ts("events reactions"). - UC-5 Campaign-preview accept flow:
task:campaignPreviewReservationputs a preview in RESERVED; preview stays out ofcurrentTaskuntil accepted (acceptedCampaignIds), then transitions to ENGAGED. Evidence:src/storeEventsWrapper.ts:243-283,537-583,772-795,tests/storeEventsWrapper.ts("campaign preview task lifecycle"). - UC-6 Fetch a domain list for a widget dropdown: Transfer/Consult widget calls
getBuddyAgents()/getQueues(); Outdial callsgetEntryPoints()/getAddressBookEntries()→ store proxies the SDK, transforms/filters, returns. Evidence:src/storeEventsWrapper.ts:924-1001,tests/storeEventsWrapper.ts.
The store is a single MobX makeAutoObservable instance. Observable slices (all in src/store.ts:23-56):
- Session / profile:
agentId,agentProfile,isAgentLoggedIn,deviceType,dialNumber,teamId,teams,loginOptions,idleCodes,wrapupCodes,featureFlags,dataCenter. - Agent state:
currentState,customState,lastStateChangeTimestamp,lastIdleCodeChangeTimestamp,showMultipleLoginAlert. - Tasks:
taskList(Record<interactionId, ITask>),currentTask,acceptedCampaignIds(Set<string>),realtimeTranscriptionData. - Call/consult control:
isMuted,callControlAudio,isQueueConsultInProgress,currentConsultQueueId,consultStartTimeStamp,isDeclineButtonEnabled,isEndConsultEnabled,allowConsultToQueue,isDigitalChannelsInitialized. - Misc:
currentTheme,cc(observable.ref— not deeply observed),isAddressBookEnabled.
Transition triggers: SDK CC/task events drive the session/agent/task slices via the wrapper's handlers (handleStateChange, handleTaskAssigned, refreshTaskList, cleanUpStore, campaign-preview handlers). Widget-initiated mutators (setDeviceType, setDialNumber, setTeamId, setState, setCurrentTheme, etc.) drive UI-local slices. All writes pass through runInAction.
- Single-threaded JS, but inherently asynchronous and event-driven: SDK events arrive at arbitrary times and mutate shared observable state. There is no ordering guarantee between unrelated SDK events.
- All state writes are wrapped in
runInAction(MobX strict mode) so each handler's mutations are applied atomically and observers see a consistent snapshot. - Idempotency: per-task listeners are registered once (guarded by
!this.taskList[id]for the incoming callback and by therealtimeTranscriptionListeners[taskId]map for transcription) and detached symmetrically inhandleTaskRemove.acceptedCampaignIdsis replaced as a newSeton each change to keep MobX reactions firing. ccisobservable.ref— the SDK object is treated as an opaque reference, never deeply observed, to avoid MobX proxying the SDK's internals.- Do NOT block inside event handlers; list fetchers are async and return promises rather than blocking the reactive update path.
- 6-second init timeout (
src/store.ts:140): only applies to thewebexConfigbootstrap path. Withinit({webex})there is no timeout — a never-ready host Webex hangs init silently. Ensure the host awaits the SDKreadyevent before callinginit({webex}). - Event enums are local copies (
store.types.ts:204-259):CC_EVENTS/TASK_EVENTSstring values must match the SDK exactly; an SDK rename will silently stop a handler from firing. - Pending campaign previews must not become
currentTask:setCurrentTaskclearscurrentTaskfor a preview in statenewthat is not inacceptedCampaignIds(storeEventsWrapper.ts:255-267). Bypassing this (e.g. calling SDK methods directly) re-introduces the bug where CallControl renders for an unaccepted preview. - Listener leaks: every
task.on(...)inregisterTaskEventListenershas a matchingtask.off(...)inhandleTaskRemove. Adding a listener in one without the other leaks handlers and can double-firerefreshTaskList. setTaskCallback/removeTaskCallbackaccept theITaskobject directly (not ataskIdstring) to avoid stalestore.taskListlookup races during React 18 StrictMode double-mount/unmount. Callers must capture and pass the task reference; passing a stale or different object orphans listeners.getBuddyAgents/getQueuesdefault args dereferencethis.currentTask.data.interaction.mediaType(storeEventsWrapper.ts:925,941): calling them with nocurrentTaskset throws. Callers should pass an explicitmediaTypewhen no task is active.@ts-expect-errormarkers tie to SDK gaps: several casts (e.g.response.teams, credentials API) are pinned toCAI-6762; removing the workaround before the SDK fix breaks the build.
- DO: route every SDK access through
store.cc.*; widgets must never import@webex/contact-centerdirectly. - DO: wrap every observable mutation in
runInAction(use the existing mutators). - DO: add a matching
task.off(...)inhandleTaskRemovefor any newtask.on(...)inregisterTaskEventListeners. - DON'T: mutate observables outside the store, or read
currentTask.data...in a default arg without a guard. - DON'T: change a
CC_EVENTS/TASK_EVENTSenum value without confirming the SDK emits that exact string.
@webex/cc-store is published and consumed by every widget package plus @webex/cc-widgets, which re-exports the store singleton. Adding an observable getter, mutator, type, or constant is a minor (additive) change. Removing/renaming any export, changing an event-enum value, or changing the init/registerCC signatures is a major (breaking) change. The TypeScript declaration surface is the export type/export lists in store.types.ts:334-403 plus index.ts. Evidence: packages/contact-center/store/src/index.ts, ai-docs/CONTRACTS.md.
Unit tests are split by source file. tests/store.ts covers the singleton defaults, registerCC profile mapping (positive) and register failure logging (negative), and all init branches including the 6s timeout reject and synchronous Webex.init throw. tests/storeEventsWrapper.ts is the largest suite: observable proxies, setState, callback register/remove (with ITask objects, not string IDs), list fetchers + getAccessToken, event reactions, hydration custom-states, refreshTaskList, setCurrentTask, and the full campaign-preview lifecycle (accepted/unaccepted, ID cleanup, type branching). A regression test verifies removeTaskCallback calls task.off() even when the task is absent from store.taskList, guarding against the orphaned-listener race. tests/task-utils.ts covers isIncomingTask (incoming / not-incoming / edge), the conference helpers, and findHoldTimestamp. tests/util.ts covers getFeatureFlags.
| Behavior / Requirement | Existing test evidence | Gap |
|---|---|---|
STORE-R-001 |
tests/store.ts |
none |
STORE-R-002 |
tests/store.ts (init) |
none |
STORE-R-003 |
tests/store.ts ("...fails to initialize") |
none |
STORE-R-004 |
tests/store.ts ("...not present") |
none |
STORE-R-005 |
tests/store.ts (register positive + negative) |
none |
STORE-R-006 |
tests/store.ts |
explicit BROWSER-filter assertion could be strengthened |
STORE-R-007 |
tests/util.ts |
no negative (unknown-key omission) case |
STORE-R-008 |
tests/storeEventsWrapper.ts (proxies, setState) |
none |
STORE-R-009 |
tests/storeEventsWrapper.ts (setCurrentTask, campaign preview) |
none |
STORE-R-010 |
tests/storeEventsWrapper.ts (refreshTaskList) |
none |
STORE-R-011 |
tests/storeEventsWrapper.ts (events reactions) |
none |
STORE-R-012 |
tests/storeEventsWrapper.ts (handleTaskRemove cleanup) |
full per-listener detach not exhaustively asserted |
STORE-R-022 |
tests/storeEventsWrapper.ts ("should remove task callback even when task is absent from store.taskList") |
none |
STORE-R-013 |
tests/storeEventsWrapper.ts (events reactions) |
none |
STORE-R-014 |
tests/storeEventsWrapper.ts (events reactions) |
none |
STORE-R-015 |
tests/storeEventsWrapper.ts (list fetchers, getAccessToken) |
address-book-disabled branch not directly asserted |
STORE-R-016 |
None found | missing telemetry-path test |
STORE-R-017 |
tests/task-utils.ts (isIncomingTask) |
none |
STORE-R-018 |
None found | getConsultStatus/getTaskStatus untested |
STORE-R-019 |
tests/task-utils.ts (conference helpers) |
none |
STORE-R-020 |
tests/task-utils.ts (findHoldTimestamp) |
findHoldStatus untested |
STORE-R-021 |
None found | handleRealtimeTranscription untested |
- Repo architecture:
ARCHITECTURE.md· Registry:SPEC_INDEX.md· Contracts:CONTRACTS.md - Coverage state & contracts baseline:
.sdd/manifest.json