Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add worklist subscription and filters #1380

Merged
merged 12 commits into from
Mar 4, 2025
Merged

Add worklist subscription and filters #1380

merged 12 commits into from
Mar 4, 2025

Conversation

paulfalgout
Copy link
Member

@paulfalgout paulfalgout commented Jan 10, 2025

Shortcut Story ID: [sc-59124]

So to implement the filters messages we have to grab a model from the store.. rather than copy this pattern to a bunch of places I moved it to the entities channel.

Then I looked at other places where this pattern existed and changed it to Model.getRelationship which will coincide eventually with simplify the various relationships once #1320 is complete.

For not the filters implementation on the ws service is overly simplistic, but I don't know what kind of filter maintenance we're going to run into yet or if it's always a simple replace.

But I'm opening this as a draft as I think I need to real-world test the socket filters before adding tests in cypress etc.

Summary by CodeRabbit

  • New Features

    • Enhanced the patient worklist with real-time notifications that immediately reflect updates for flows and actions.
    • Introduced improved filtering for a more accurate and responsive display of data.
    • Streamlined subscription mechanism to accommodate filters during WebSocket communications.
  • Bug Fixes

    • Strengthened error handling during live data communications, reducing the risk of processing incomplete updates.
  • Tests

    • Added automated tests to ensure that real-time notifications trigger the correct UI updates for patient worklist changes.
    • Implemented specifications for socket notifications related to flows and actions to verify UI updates.

@paulfalgout paulfalgout requested a review from nmajor25 January 10, 2025 07:26
Copy link

coderabbitai bot commented Jan 10, 2025

Walkthrough

The changes enhance real-time update capabilities and message handling across several modules. In the worklist application and WebSocket service, new subscription methods are introduced to process messages with added filters, invoking dedicated data fetching methods. The Backbone model now tracks fetching state and queues messages appropriately, while the entity action fetch method is updated to request specific attributes. Integration tests are added to verify proper UI updates after socket notifications.

Changes

File(s) Change Summary
src/js/apps/.../worklist_app.js
src/js/services/ws.js
Added a subscribe() method (and updated its signature) to establish WebSocket channels with filters. Introduced private methods _fetchFlow(model) and _fetchAction(model) to process messages based on their category.
src/js/base/model.js Introduced a private _isFetching flag to track fetch state. Updated fetch to set/reset this flag, added isFetching(), queueMessage(data), and enhanced message processing via _handleMessage().
src/js/entities-service/actions.js Updated fetchAction(id) to include a fields object specifying the name and state attributes for fetched flows.
test/integration/patients/worklist/worklist.js Added integration tests simulating socket notifications for flows and actions to confirm UI updates and proper handling of real-time data.

Sequence Diagram(s)

Loading
sequenceDiagram
    participant App as Worklist App
    participant WS as WebSocket Service
    participant Server as Socket Server

    App->>WS: subscribe(resources, {shouldPersist, filters})
    WS->>Server: Send subscribe request with filters and clientKey
    Server-->>WS: Acknowledge subscription
    WS-->>App: Receive message notification
    alt Message category is "flow"
        App->>App: _fetchFlow(model)
    else Message category is "action"
        App->>App: _fetchAction(model)
    end

Possibly related PRs

  • Restart closed websocket if still subscribed #1373: The changes in the main PR introduce new methods for handling WebSocket subscriptions and data fetching, while the retrieved PR focuses on enhancing the WebSocket service's ability to restart subscriptions after a connection closure, indicating a direct relationship in their handling of WebSocket functionality.
  • Support websocket workspace #1376: The changes in the main PR are related to the modifications in the WebSocket subscription logic and message handling, particularly through the introduction of the subscribe() method and the handling of filters, which aligns with the updates made in the retrieved PR regarding the subscribe method's implementation.

Suggested reviewers

  • paulfalgout

Poem

I hop through code with rhythmic grace,
New subscribe methods set the pace.
Flows and actions now take flight,
Messages queued in the silent night.
Coding magic in every byte—hop on, what a sight! 🐇

✨ Finishing Touches
  • 📝 Generate Docstrings

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Sorry, something went wrong.

Copy link

cypress bot commented Jan 10, 2025

RoundingWell Care Ops Frontend    Run #7316

Run Properties:  status check passed Passed #7316  •  git commit 0222306fd0: Add `.catch()` to base model fetch
Project RoundingWell Care Ops Frontend
Branch Review socket-filters
Run status status check passed Passed #7316
Run duration 02m 49s
Commit git commit 0222306fd0: Add `.catch()` to base model fetch
Committer Nick Major
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 316
View all changes introduced in this branch ↗︎

@coveralls
Copy link

coveralls commented Jan 10, 2025

Pull Request Test Coverage Report for Build 7e251c34-b1ca-4403-aab9-e03eb8c5d664

Details

  • 30 of 30 (100.0%) changed or added relevant lines in 3 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 100.0%

Totals Coverage Status
Change from base Build 840a2d60-921c-413e-be7f-0ea9f9e51a27: 0.0%
Covered Lines: 6129
Relevant Lines: 6129

💛 - Coveralls

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
There’s probably a better way to do this, but might need more use cases first
Apparently Bref requires something be sent at point when there’s nothing to send.. so we’ll just exit out here.

But I can’t test it because it doesn’t seem possible to send nothing in the node websocket setup.
flow_states aren’t able to be filtered on the BE so we’re doing an extra filter here
Set to `true` when model is being fetched

In worklist app, we can use that to not do duplicate `model.fetch()` requests after ws notifications are received
this.listenTo(channel, 'message', ({ category, payload }) => {
if (category !== 'ResourceCreated') return;

const model = Radio.request('entities', 'get:store', payload.resource);
Copy link
Contributor

Choose a reason for hiding this comment

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

I wanted to do this ☝️ in the onMessage() of the /js/services/ws.js file.

Specifically I wanted to do something like:

if (data.category === 'ResourceCreated') {
  model = Radio.request('entities', 'get:store', data.payload.resource);
  channel.trigger('message', data, model);
  return;
}

But that would break our handling of the ResourceCreated ws notification for actions on the patient flow page.

Both of these ResourceCreated's have both a data.resource and data.payload.resource. So I'm not sure how to differentiate between the two in that onMessage() function.

…ns on worklists

Instead of `data.resource`, which contains the patient data
@@ -161,14 +161,21 @@ export default App.extend({

channel.request('subscribe', this.collection.models, { filters: { [filterKey]: this.filters } });

this.listenTo(channel, 'message', (message, model) => {
this.listenTo(channel, 'message', ({ category, resource, payload }) => {
if (!category) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

In my local dev environment I'm getting a ws notification that contains a lambda error. When that happens this listenTo is called with category, resource, and payload as undefined.

I don't know if this is something that could happen in production or not.

Screenshot 2025-02-24 at 4 50 48 PM

Copy link
Member Author

Choose a reason for hiding this comment

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

we shouldn't need the if here. should be able to handle those errors prior to triggering the message

Copy link
Contributor

@nmajor25 nmajor25 Feb 25, 2025

Choose a reason for hiding this comment

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

I'm looking at that currently. Would be in the next commit. I have a event.data.category check added to the onMessage() here.

So at the top of that function it would be:

if (!event.data || !event.data.category) return;

But still need to test it locally and think about it a little more.

if (!this.notifications) {
this.notifications = [data];
return;
}
Copy link
Contributor

@nmajor25 nmajor25 Feb 24, 2025

Choose a reason for hiding this comment

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

Is there a way to set model.notifications: [] initially for each model? Do we want to do that?

If we did that we wouldn't have to check if this.notifications exists here. And just do the push().

Copy link
Contributor

@nmajor25 nmajor25 Feb 24, 2025

Choose a reason for hiding this comment

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

And wasn't sure what to use for naming. But went with model.notifications for now.

Copy link
Member Author

Choose a reason for hiding this comment

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

there is not a good place to instantiate this, but it should probably be _notifications and it should be {}. where this.notifications['StateChanged'] would always be the latest StateChanged

Copy link
Contributor

Choose a reason for hiding this comment

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

_notifications naming makes sense.

But why an object {} instead of an array [{}]?

Copy link
Member Author

Choose a reason for hiding this comment

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

because we only need to run the most recent of a particular category

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah okay, right. Using an object we can just replace the previous notification in the queue if one already exists with the same notification category (StateChanged, OwnerChanged, etc.).

@@ -99,6 +103,9 @@ export default App.extend({
},

onMessage(event) {
/* istanbul ignore next: Can't test this bref functionality in node websockets */
if (!event.data) return;

Copy link
Member Author

Choose a reason for hiding this comment

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

I think we should handle event.data.error near the pong return.. such that we trigger error instead of message and not have a if (!category) check in the message handler.

this.notifications.push(data);
},
handleMessage(data) {
if (this.isFetching()) {
Copy link
Member Author

Choose a reason for hiding this comment

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

I think extending the attributes with the updated_at should happen here so that it's stored with the queue.. that timestamp is when the action occurred, not when it was processed.

if (!this.notifications) {
this.notifications = [data];
return;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

there is not a good place to instantiate this, but it should probably be _notifications and it should be {}. where this.notifications['StateChanged'] would always be the latest StateChanged

this.collection.add(model);
channel.request('add', model);

if (model.notifications) {
Copy link
Member Author

Choose a reason for hiding this comment

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

we should either entirely handle this inside the base model, or entirely handle it here.. perhaps the base model is better.. I can't think of why we wouldn't always want this to happen

Copy link
Contributor

@nmajor25 nmajor25 Feb 25, 2025

Choose a reason for hiding this comment

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

I think it would work to do this in the base model.

But does this.collection.add(model) need to happen before we handle the queued notifications? Or does that order not matter?

For example if the order of things happened like this, would the correct flow.state show in the UI?

  1. A StateChanged notification is queued while a new flow is fetched.
  2. Flow fetch finishes.
  3. Queued StateChanged ws notification is handled and state is updated on the flow model.
  4. Flow model is added to the worklist collection via this.collection.add(model), which makes it show in the UI.

Does the order of 3 & 4 matter? My assumptions is that it doesn't matter. Either way the flow's state should show correctly in the UI.

Copy link
Member Author

Choose a reason for hiding this comment

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

better to do data before render

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep 👍

// Resolve with entity if successful
return fetcher.then(response => {
this._isFetching = false;

if (this._notifications) {
Copy link
Member Author

Choose a reason for hiding this comment

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

no need for the if.. I think each(this._notifications, this._handleMessage); is fine here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, not needed 👍

@@ -161,14 +161,21 @@ export default App.extend({

channel.request('subscribe', this.collection.models, { filters: { [filterKey]: this.filters } });

this.listenTo(channel, 'message', (message, model) => {
this.listenTo(channel, 'message', ({ category, resource, payload }) => {
if (!category) return;
Copy link
Member Author

Choose a reason for hiding this comment

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

we shouldn't need the if here. should be able to handle those errors prior to triggering the message

@@ -98,12 +109,30 @@ export default Backbone.Model.extend(extend({

return isFunction(messages[category]) ? messages[category] : this[messages[category]];
},
handleMessage({ category, resource, author, payload }) {
payload.attributes = extend({}, payload.attributes, { updated_at: dayjs.utc().format() });
queueMessage(data) {
Copy link
Member Author

Choose a reason for hiding this comment

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

So this function looks complex to me, but I get why.. but we could start using some of the newer operators to make this a bit simpler:

queueMessage(data) {
 return (this._notifications ||= {})[data.category] = data;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

That's pretty cool, didn't know ||= existed until now 👍

But vite-plugin-eslint doesn't support it. It's using eslint v8.21.0 (link). Eslint added support logical assignment operators in v8.24.0 (source).

That package's repo seems to be abandoned, so we probably want to switch to something different.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's just the dev dependency. We're using 9 and it's a peer dependency https://github.com/gxmari007/vite-plugin-eslint/blob/main/package.json#L60

Copy link
Member Author

Choose a reason for hiding this comment

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

FWIW it looks like the vite community doesn't run eslint as part of the build process. They're using it in pre-commit pre-build hooks.. I find that mildly annoying

Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW backend runs static analysis (including lint) on commit and on test (CI).


if (this.collection.get(model) || model.isFetching()) return;

model.fetch().then(() => {
Copy link
Member Author

Choose a reason for hiding this comment

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

I think this'll be:

const include = [
      'program-action.program',
      'flow.program-flow.program',
    ].join()

const data = {
  fields: { flows: ['name', 'state'] },
  include,
};

model.fetch({ data }).then(...

Honestly that's probably enough logic to move it to a Radio.request('entities', 'fetch:action..

Or maybe just add the fields to 'fetch:actions:model' and use that over model.fetch here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Great thanks, I just got this working in my local dev environment.

For now I'm just doing the following:

  // js/apps/patients/worklist/worklist_app.js

  _getWsFetchOptions(filterKey) {
    if (filterKey === 'flows') return {};

    return {
      data: {
        fields: { flows: ['name', 'state'] },
        include: ['program-action.program', 'flow.program-flow.program'].join(),
      },
    };
  },
  subscribe() {

    . . .

    const options = this._getWsFetchOptions(filterKey);

    model.fetch(options).then(() => {

       . . .

    })
  },

Copy link
Contributor

Choose a reason for hiding this comment

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

Having this working now allows me to finish creating the initial tests for the action version of the worklist. After that I'll come back and do some refactoring on this and other parts too.

@@ -151,6 +154,29 @@ export default App.extend({

this.showList();
},
subscribe() {
Copy link
Member Author

Choose a reason for hiding this comment

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

Alright I think this whole function needs refactoring..

we should probably just split based on filterKey and hardcode types for this one..

also somewhere we can call break this up into another function

subscribe() {
  this.listenTo(... () => {
    fetch().then(() => {. // <- one or both of these could be a separate handler..

But I think we still want to check the store for the resource.. but if we need to fetch it, as mentioned below.. switch to either the action or flow entity request rather than adding the fetch specifications here for include/fields.

Copy link
Contributor

Choose a reason for hiding this comment

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

I took a crack at refactoring this in 836c7c0, let me know what you think.

…on for that model. And then handle those ws notifications once the fetch finishes
So the app doesnt handle ws notifications that are errors, only actual entity events
Copy link
Contributor

@nmajor25 nmajor25 Feb 27, 2025

Choose a reason for hiding this comment

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

Summary of what I'm testing here.

Flow list notifications:

  • Load owned-by worklist with one flow
    • Test each ws notification category that applies on preloaded flow
      • NameChanged
        • Verify updated_at date
      • OwnerChanged
      • StateChanged
    • Add flow to list via ResourceCreated ws notification (w/ trailing StateChanged msg)
      • Verify included is not in request URL string
      • Verify flow count
      • Verify flow was added to list
      • Verify trailing StateChanged was queued and processed after model fetch finishes
      • Verify new flow is subscribed to future ws notifications
    • Add new flow to list via StateChanged ws notification
    • Add new flow to list via OwnerChanged ws notification

Action list notifications:

  • Load owned-by worklist with one action
    • Test each ws notification category that applies on preloaded action
      • NameChanged
        • Verify updated_at date
      • OwnerChanged
      • ActionDueChanged
      • DetailsChanged
      • StateChanged
      • CommentAdded
      • AttachmentAdded
      • SharingUpdated
    • Add action to list via ResourceCreated ws notification (w/ trailing StateChanged msg)
      • Verify included data is in request URL string
      • Verify action count
      • Verify action was added to list
      • Verify trailing StateChanged was queued and processed after model fetch finishes
      • Verify new action is subscribed to future ws notifications
    • Add new action to list via StateChanged ws notification
    • Add new action to list via OwnerChanged ws notification
    • Handle ws notification for new action with a flow state not in the current filters
      • In this scenario, the action should not be added to list

Copy link
Member Author

@paulfalgout paulfalgout left a comment

Choose a reason for hiding this comment

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

looking pretty good.

id: 'owned-by',
listType: 'flows',
flowsSortId: 'sortCreatedDesc',
clinicianId: '11111',
Copy link
Member Author

Choose a reason for hiding this comment

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

Don't love using new string ids.. is this getCurrentClinician().id?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, that's the current clinician (i.e. getCurrentClinician()). I updated the tests to reference that directly instead of using 11111.

id: testPatient1.id,
},
payload: {
resource: {
Copy link
Member Author

Choose a reason for hiding this comment

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

I do badly want destructing within an object like

resource: { id, type } = testPatient1, 

but alas none of these types of proposals go anywhere right now in TC39 😿

we could make some sort of generic helper. function foo({ id, type ]) => { id, type };
resource: foo(testNewSocketFlow)

thought for another day

Copy link
Contributor

Choose a reason for hiding this comment

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

Ya that would be pretty nice 👍

relationships: {
state: getRelationship(stateTodo),
flow: getRelationship(testFlow),
files: getRelationship([]),
Copy link
Member Author

Choose a reason for hiding this comment

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

it's ok to do so, but I suspect we don't need to set a lot of this for the tests? details? due_time? duration. files relationship? If they're just random values from getAction does that potentially cause flake? Ideally were setting whatever minimum test data we need and letting getAction do the rest

Copy link
Contributor

Choose a reason for hiding this comment

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

.routeFormByAction()
.routeFormDefinition()
.routeLatestFormResponse()
.visitOnClock('/worklist/owned-by', { now: testTs() })
Copy link
Member Author

Choose a reason for hiding this comment

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

the visitOnClock is to set the time to now or why is this needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's to avoid flakiness when checking that the updated_at timestamp is updated correctly in the UI.

First introduced in this commit: 8eb7d81.

Copy link
Member Author

@paulfalgout paulfalgout left a comment

Choose a reason for hiding this comment

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

couple of small things


this.collection.add(model);

Radio.channel('ws').request('add', model);
Copy link
Member Author

Choose a reason for hiding this comment

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

Radio.request('ws', 'add',... here and above

Copy link
Contributor

Choose a reason for hiding this comment

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

Updated 👍

return;
}

if (filterKey === 'actions') {
Copy link
Member Author

Choose a reason for hiding this comment

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

I think you'll need to remove the if here and have a default, otherwise we'll have an uncovered else branch

Copy link
Contributor

Choose a reason for hiding this comment

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

Updated 👍

@paulfalgout paulfalgout marked this pull request as ready for review March 3, 2025 04:04
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (7)
test/integration/patients/worklist/worklist.js (2)

306-587: Consider splitting this large test into smaller, focused specs.

This end-to-end scenario is comprehensive; however, multiple smaller test cases can improve readability and maintainability, making it easier to diagnose failures.


1134-1605: Promote reuse of repeated WebSocket steps or test data.

The extensive real-time action notification test could benefit from extracting repeated patterns (e.g., flow creation, queued notifications, etc.) into shared helper functions or custom commands.

src/js/services/ws.js (2)

40-47: Exercise caution when sending large resource payloads.

Building the data object with full this.resources and optional filters is flexible. However, if resources or filters grow significantly, consider strategies to prevent large payload overhead (e.g., pagination or chunked updates).


132-135: Offer help implementing a more robust filter management approach.

A single this.filters might be insufficient for multiple resource subscriptions. Happy to propose or implement a design that stores each subscription’s filters more granularly.

src/js/apps/patients/worklist/worklist_app.js (2)

181-188: Add error handling for _fetchFlow.
At present, the .then() callback does not handle rejections. Consider adding a .catch() block or similar mechanism to surface errors if the fetch fails (e.g., network error or invalid data). This ensures that the app can gracefully handle fetch failures and provide feedback.

 _fetchFlow(model) {
   const fetchFlow = Radio.request('entities', 'fetch:flows:model', model.id);

+  fetchFlow
+    .then(() => {
       this.collection.add(model);
       Radio.channel('ws').request('add', model);
+    })
+    .catch((error) => {
+      // handle or log error, e.g. show an alert or retry
+    });
 }

189-201: Check getFlow() safely and handle fetch errors.
If model.getFlow() is undefined or missing a valid state, model.getFlow().getState() will throw an error. Also, add a .catch() block to address potential fetch failures.

_fetchAction(model) {
  const flowStates = this.getState('flowStates');

  const fetchAction = Radio.request('entities', 'fetch:actions:model', model.id);

- fetchAction.then(() => {
-   if (!flowStates.includes(model.getFlow().getState().id)) return;
+ fetchAction
+   .then(() => {
+     if (!model.getFlow() || !model.getFlow().getState()) return;
+     if (!flowStates.includes(model.getFlow().getState().id)) return;

     this.collection.add(model);
     Radio.channel('ws').request('add', model);
+   })
+   .catch((error) => {
+     // handle or log error appropriately
+   });
}
src/js/base/model.js (1)

110-119: Consider clearing processed notifications or trimming outdated ones.
New messages overwrite notifications of the same category with queueMessage({ category, ... }). After processing them in _handleMessage, consider cleaning up _notifications to prevent potential buildup of stale notifications, especially if many categories accumulate.

Also applies to: 126-135

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f387a7 and 836c7c0.

📒 Files selected for processing (5)
  • src/js/apps/patients/worklist/worklist_app.js (4 hunks)
  • src/js/base/model.js (3 hunks)
  • src/js/entities-service/actions.js (1 hunks)
  • src/js/services/ws.js (3 hunks)
  • test/integration/patients/worklist/worklist.js (3 hunks)
🔇 Additional comments (7)
src/js/entities-service/actions.js (1)

20-25: Looks good. Consider verifying backend support for partial field retrieval.

The addition of fields.flows aligns with JSON:API conventions for sparse fieldsets. Ensure that the backend expects fields[flows]=name,state to avoid unexpected behavior.

test/integration/patients/worklist/worklist.js (1)

3-3: Good use of NIL UUID constants.

Importing NIL as NIL_UUID helps avoid magic strings for “no-owner” or “no-clinician” use cases.

src/js/services/ws.js (2)

106-108: Early return for empty messages looks fine.

Skipping processing for empty event.data is a safe fallback. Ensure the server never sends essential data in an empty payload.


115-115: Verify that skipping messages without a category is intentional.

This condition silently ignores all non-pong messages lacking a category. Confirm no other message types are impacted.

src/js/apps/patients/worklist/worklist_app.js (3)

129-132: Good approach for managing filters.
Storing the filter in a local property (this.filters) ensures that the filter is consistently referenced throughout the app, preventing repetitive calls to this.getState().getEntityFilter().


157-179: Validate model availability before fetching.
There's a possibility that Radio.request('entities', 'get:store', modelResource) might return undefined if the resource is not in the store. Consider verifying that model exists before proceeding, to avoid potential errors when calling model.isFetching().


261-261: No significant change detected.

Comment on lines 39 to 44
this._isFetching = true;

// Resolve with entity if successful
return fetcher.then(response => {
this._isFetching = false;

Copy link

@coderabbitai coderabbitai bot Mar 3, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure _isFetching is reset on fetch errors.
Currently, _isFetching is only set to false on success. If the request fails or is aborted, _isFetching might remain true indefinitely, preventing message handling. Consider adding a .catch() to handle failures and reset _isFetching.

fetch(options) {
  const fetcher = Backbone.Model.prototype.fetch.call(this, extend({ abort: true }, options));

  this._isFetching = true;

- return fetcher.then(response => {
+ return fetcher
+   .then(response => {
      this._isFetching = false;
      each(this._notifications, this._handleMessage, this);
      if (!response || response.ok) return this;
      return response;
+   })
+   .catch(error => {
+     this._isFetching = false;
+     // Optionally, handle or log the error
+     throw error;
+   });
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this._isFetching = true;
// Resolve with entity if successful
return fetcher.then(response => {
this._isFetching = false;
fetch(options) {
const fetcher = Backbone.Model.prototype.fetch.call(this, extend({ abort: true }, options));
this._isFetching = true;
return fetcher
.then(response => {
this._isFetching = false;
each(this._notifications, this._handleMessage, this);
if (!response || response.ok) return this;
return response;
})
.catch(error => {
this._isFetching = false;
// Optionally, handle or log the error
throw error;
});
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Correct me if I'm wrong here @paulfalgout, but I think we'd want this._isFetching to remain true in the scenario of a fetch error. Since we wouldn't want the model's queued ws notifications to be processed. So I think the code we current have is good.

Copy link

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Member Author

Choose a reason for hiding this comment

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

hmm no I think we should add this catch with the rethrow of the error. It is more correct for the state of the base model, regardless of how it affects us downstream. if a fetch fails though, we're not necessarily doing anyhing in order to process the queue correctly anyhow.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's been added in commit 0222306.

Nice catch @coderabbitai 👍

Copy link

Choose a reason for hiding this comment

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

@nmajor25 Thank you for implementing the fix! Adding the .catch() block ensures that _isFetching will be properly reset even when fetch operations fail, which makes the code more robust and prevents potential issues with message handling. It's great to see that the team agreed on the approach and incorporated the suggestion so quickly.


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/js/apps/patients/worklist/worklist_app.js (2)

169-178: Unnecessary return statements can be removed.

There are explicit return statements after calling _fetchFlow and _fetchAction that aren't necessary since they're the last statements in their respective code blocks. Consider removing them for cleaner code.

-      this._fetchFlow(model);
-
-      return;
+      this._fetchFlow(model);
+      return;
-      this._fetchAction(model);
-
-      return;
+      this._fetchAction(model);

190-190: Consider caching flowStates for performance.

The call to this.getState('flowStates') might be more efficiently handled by caching this value at the class level, similar to how you're handling this.filters, especially if it's accessed frequently.

  subscribe() {
    const channel = Radio.channel('ws');
    const filterKey = this.getState().getType();
+   this.flowStates = this.getState('flowStates');

    channel.request('subscribe', this.collection.models, { filters: { [filterKey]: this.filters } });

    // ... rest of the method
  },
  
  _fetchAction(model) {
-   const flowStates = this.getState('flowStates');
+   const flowStates = this.flowStates;

    // ... rest of the method
  },
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 836c7c0 and 0c5aa0c.

📒 Files selected for processing (2)
  • src/js/apps/patients/worklist/worklist_app.js (4 hunks)
  • src/js/entities-service/actions.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/js/entities-service/actions.js
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: test-build
🔇 Additional comments (8)
src/js/apps/patients/worklist/worklist_app.js (8)

128-133: Good centralization of filter state.

Storing this.filters as a class property improves code organization and avoids redundant getter calls, making the code more maintainable.


147-148: Subscription initialization looks good.

Adding the WebSocket subscription during the app start process ensures real-time updates are properly configured.


161-161: Approve filter-based WebSocket subscription.

Using filters when subscribing to WebSocket updates is a good approach to ensure you only receive relevant notifications, reducing unnecessary processing.


163-168: Good optimization to avoid duplicate processing.

The check for this.collection.get(model) || model.isFetching() prevents redundant processing of models that are already in the collection or currently being fetched, which is an important optimization.


180-188: Good implementation of flow fetching.

The _fetchFlow method effectively fetches the flow model and adds it to the collection upon successful retrieval. The call to Radio.request('ws', 'add', model) ensures that the WebSocket service is aware of the new model.


189-201: Flow state validation in action fetching is handled correctly.

The _fetchAction method properly validates that the action's flow state is included in the allowed flow states before adding it to the collection. This ensures that only relevant actions are displayed in the worklist.


157-201:

❓ Verification inconclusive

Consider adding tests for WebSocket subscription.

The PR objectives mention that tests will be added using Cypress. Make sure to include tests that verify:

  1. WebSocket subscription with filters works correctly
  2. New models are properly fetched and added to the collection
  3. Flow state validation prevents irrelevant actions from being added

This is important since real-time updates are a critical feature that needs reliable behavior.


🏁 Script executed:

#!/bin/bash
# Check for existing WebSocket tests

echo "Looking for existing WebSocket tests..."
fd -e js -e jsx test.*ws js/test

Length of output: 220


Action: Add Cypress tests for WebSocket subscriptions

The current PR does not appear to include any tests verifying the WebSocket subscription behavior. Please add Cypress tests that ensure:

  • The WebSocket subscription applies filters correctly.
  • New models are fetched and added to the collection as expected.
  • Flow state validation prevents irrelevant models/actions from being processed.

Note: The initial search under “js/test” using fd did not yield any results, so please verify if the tests have been placed in a different directory (e.g., “cypress/integration”).


186-186:

✅ Verification successful

Verify WebSocket 'add' method implementation.

The code calls Radio.request('ws', 'add', model) after adding models to the collection, but there was a previous review comment suggesting to use 'add' instead of another term. Let's verify this is the correct method name in the WebSocket service.

Also applies to: 199-199


🏁 Script executed:

#!/bin/bash
# Check for the 'add' method in the WebSocket service

echo "Checking for 'add' method in WebSocket service..."
rg -A 3 -B 3 "add.*: function\(" src/js/services/ws.js

Length of output: 171


I'll run an additional script to search for an ES6-style method definition of the add method in the WebSocket service. The previous search looked specifically for the pattern add: function(, which might not catch shorthand method definitions. Let's try searching for any occurrence of add( in the file:


🏁 Script executed:

#!/bin/bash
echo "Searching for ES6-style definition of 'add' in WebSocket service (src/js/services/ws.js)..."
rg -n "add\s*\(" src/js/services/ws.js

Length of output: 378


Confirmed: The WebSocket service correctly implements the add method.

  • The ES6-style add(resources, { shouldPersist } = {}) method is present in src/js/services/ws.js (around line 151).
  • The usage of Radio.request('ws', 'add', model) in src/js/apps/patients/worklist/worklist_app.js (lines 186 and 199) correctly calls this method.

No further changes are necessary.

Comment on lines +157 to +179
subscribe() {
const channel = Radio.channel('ws');
const filterKey = this.getState().getType();

channel.request('subscribe', this.collection.models, { filters: { [filterKey]: this.filters } });

this.listenTo(channel, 'message', ({ category, resource, payload }) => {
const modelResource = category === 'ResourceCreated' ? payload.resource : resource;
const model = Radio.request('entities', 'get:store', modelResource);

if (this.collection.get(model) || model.isFetching()) return;

if (filterKey === 'flows') {
this._fetchFlow(model);

return;
}

this._fetchAction(model);

return;
});
},
Copy link

@coderabbitai coderabbitai bot Mar 3, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Consider adding error handling for undefined message properties.

The message handler doesn't check if category, resource, or payload are undefined, which could cause runtime errors. Based on previous review comments, there have been cases where lambda errors resulted in undefined properties in WebSocket notifications.


🏁 Script executed:

#!/bin/bash
# Check for any existing error handling in the WebSocket service

# Look for error handling in the WebSocket service
echo "Checking for error handling in WebSocket service..."
rg -A 5 -B 5 "onMessage" src/js/services/ws.js

Length of output: 917


Action Required: Add Defensive Checks for Message Properties in the Event Handler

  • In src/js/apps/patients/worklist/worklist_app.js (lines 157‑179), the event listener for the 'message' event destructures { category, resource, payload } directly without verifying their existence.

  • Although the WebSocket service (in src/js/services/ws.js) checks for the presence of event.data in its onMessage handler, this alone does not guarantee that the message object passed to the worklist handler will include valid category, resource, or payload properties.

  • Consider adding guard clauses at the start of the listener callback to ensure that these properties are defined before proceeding. For example:

    this.listenTo(channel, 'message', ({ category, resource, payload }) => {
      if (
        typeof category === 'undefined' ||
        typeof payload === 'undefined' ||
        (category === 'ResourceCreated' && typeof payload.resource === 'undefined') ||
        (category !== 'ResourceCreated' && typeof resource === 'undefined')
      ) {
        console.warn('Received message with missing properties', { category, resource, payload });
        return;
      }
    
      const modelResource = category === 'ResourceCreated' ? payload.resource : resource;
      const model = Radio.request('entities', 'get:store', modelResource);
    
      if (this.collection.get(model) || model.isFetching()) return;
    
      if (filterKey === 'flows') {
        this._fetchFlow(model);
        return;
      }
    
      this._fetchAction(model);
    });
  • This update will help avoid runtime errors when WebSocket messages fail to include the expected properties.

Copy link
Contributor

Choose a reason for hiding this comment

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

In src/js/apps/patients/worklist/worklist_app.js we now check that the data contains a category. And we don't handle that web socket notification if it doesn't have that.

If there's no category, that's a good indication that the data isn't going to be a valid web socket notification.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

Looking at the WebSocket service implementation to see the changes related to category checking.

@nmajor25 Thank you for explaining the check that was implemented. You're right - checking for the presence of category in the WebSocket notification is an effective way to validate that the data is a proper notification worth processing.

I see that this check was implemented in the WebSocket service's onMessage method, which provides a centralized validation before any message is dispatched to subscribers. This approach is more efficient than adding defensive checks in each individual subscriber since it prevents invalid messages from propagating in the first place.

By filtering out notifications without a category at the service level, you've effectively addressed the concern I raised about potential runtime errors from undefined properties. This is a clean solution that maintains the simplicity of your subscriber code while still ensuring robustness against malformed messages.


🏁 Script executed:

#!/bin/bash
# Check the updated onMessage method in the WebSocket service
rg -A 10 "onMessage" src/js/services/ws.js

Length of output: 702


@nmajor25 You're absolutely right. I see from the WebSocket service code that there's already a centralized validation check in place:

onMessage(event) {
  /* istanbul ignore next: Can't test this bref functionality in node websockets */
  if (!event.data) return;

  let model;

  const channel = this.getChannel();

  const data = JSON.parse(event.data);

  if (!data.category || data.name === 'pong') return;

This is an excellent approach - validating the message data at the service level with the check for !data.category || data.name === 'pong' prevents invalid messages from ever reaching subscribers. Since this validation happens centrally, there's no need for redundant checks in each subscriber like the worklist app.

This is a clean architectural choice that:

  1. Prevents propagation of invalid messages
  2. Keeps subscriber code simpler and more focused
  3. Centralizes validation logic in one place

Thanks for clarifying how the validation is implemented in your system!

@nmajor25
Copy link
Contributor

nmajor25 commented Mar 3, 2025

@paulfalgout coveralls coverage looks good 👍

When that happens, set `_isFetching = false` and throw the error
Copy link
Member Author

@paulfalgout paulfalgout left a comment

Choose a reason for hiding this comment

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

approved on my end

@nmajor25 nmajor25 self-requested a review March 4, 2025 16:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/js/base/model.js (1)

116-125: Consider simplifying the notification queue initialization.

The code correctly implements notification queueing with the discussed design of using object properties keyed by category. However, the initialization logic could be simplified.

queueMessage(data) {
  const category = data.category;

-  if (!this._notifications) {
-    this._notifications = { [category]: data };
-    return;
-  }
-
-  this._notifications[category] = data;
+  this._notifications = this._notifications || {};
+  this._notifications[category] = data;
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c5aa0c and 0222306.

📒 Files selected for processing (1)
  • src/js/base/model.js (3 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: test-build
🔇 Additional comments (6)
src/js/base/model.js (6)

1-1: Clean import addition for required utility.

The addition of each from underscore is correctly added to support the new notification processing functionality.


39-40: LGTM: Well-designed state tracking introduction.

The addition of _isFetching private property creates a clear tracking mechanism for the model's fetch state.


42-56: Robust fetch handling with proper state management.

This implementation correctly handles both success and error scenarios, ensuring _isFetching is reset properly in both cases. The error handling is particularly important to avoid leaving models in an incorrect state if a fetch fails.

The notification processing after a fetch completes is a clean solution to the message queuing problem.


58-60: Simple and effective public accessor method.

The isFetching() method provides a clean public API to check the model's fetching state without directly accessing the private _isFetching property.


126-131: Well-designed message handling logic.

The private _handleMessage method is well-structured and properly delegates to specific handlers while also triggering a general message event.


132-141:

✅ Verification successful

Ensure payload mutation is intentional.

The handleMessage method now extends the payload with a timestamp and implements the fetch-aware message queueing logic correctly. However, note that this directly mutates the incoming payload object.


🏁 Script executed:

#!/bin/bash
# Search for other places in the codebase that might be affected by the payload mutation

# Check for direct references to payload.attributes that might expect the original structure
echo "Searching for direct references to payload.attributes..."
rg -A 3 -B 3 "payload\.attributes" --type js

Length of output: 641


Payload Mutation Verified: No Unintended Side Effects

The change in src/js/base/model.js where payload.attributes is extended with a timestamp (using dayjs.utc().format()) has been verified. The search results confirm that the mutation is localized to this file and no other parts of the codebase rely on an unmodified payload.attributes. Please ensure that this inline mutation approach is consistent with our design decisions regarding object immutability and side effects.

@paulfalgout
Copy link
Member Author

The error is coveralls 504ing.

@paulfalgout paulfalgout merged commit c9f4177 into develop Mar 4, 2025
5 of 6 checks passed
@paulfalgout paulfalgout deleted the socket-filters branch March 4, 2025 16:52
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.

None yet

4 participants