Skip to content

Reactive pos-label and pos-description#197

Open
jg10-mastodon-social wants to merge 22 commits intomainfrom
feat/reactive-label-description
Open

Reactive pos-label and pos-description#197
jg10-mastodon-social wants to merge 22 commits intomainfrom
feat/reactive-label-description

Conversation

@jg10-mastodon-social
Copy link
Copy Markdown
Collaborator

@jg10-mastodon-social jg10-mastodon-social commented Mar 1, 2026

Closes #175

  • Observable for anyValue, observing subject+predicates
  • Observable for description: wrapper around anyValue
  • Observable for label: wrapper around anyValue
  • Pos-label uses new observable
  • Update unit tests using pos-label and pos-description mocks:
    • mockPodOs: pos-login
    • pos-list
    • pos-resource
    • pos-rich-link
    • pos-switch
  • Pos-description uses new observable
  • Pos-value uses observeAnyValue
  • Tests have been written
    • all new code is covered by unit tests
    • the happy path of a new feature is covered by an end-to-end test
      • Will implement with dynamic pane feature
    • manual explorative tests have been performed
  • all dependencies are updated to the latest patch version at minimum
  • the CI pipeline passes
  • documentation is up-to-date
    • TSDoc style comments on important functions, properties and events
    • stories for new PodOS elements have been added to storybook
      • N/A - no new elements
    • Readme.md files of PodOS elements have been re-generated
    • N/A architectural decisions are documented as an ADR
    • Changelogs are updated according to Keep a Changelog

),
startWith(this.anyValue(...predicateUris)),
);
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is a bit different to other observe functions because I've tried to use the stream of changes directly and avoid unnecessary calls to anyValue.

I should perhaps have implemented this through multiple commits to improve clarity, but I did attempt to follow TDD so the tests should correspond to features of this function.

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.

question: Why is it important that anyValue is not called? Is it that expensive?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I've had performance issues in my experiments generally but I don't yet know whether anyValue is a problem specifically.

rdflib.js hopefully uses indexes, but whatever it does internally to search through the store to lookup the subject and predicate would be much more expensive than just reacting to the streamed value.

So this might be premature optimisation and we could call anyValue here too - though it would also be useful to give a precedent of what can be done with the rxjs architecture. If rxjs ends up being too confusing maybe we should be looking at other reactive architectures for the future.

const observable = thing.observeLabel();
observable.subscribe(subscriber);

expect(subscriber.mock.lastCall).toEqual([thing.label()]);
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

While observeLabel doesn't call label at all, it does call labelFromUri so I thought this might be a sufficient test without duplicating all of "if nothing is found in the store".

From a TDD perspective, I wrote this as a failing test before implementing

.pipe(map((value) => value ?? labelFromUri(this.uri)));

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.

I think this is reasonable

@jg10-mastodon-social jg10-mastodon-social force-pushed the feat/reactive-label-description branch from 25ffd0b to e85a6f2 Compare March 21, 2026 00:10
@jg10-mastodon-social
Copy link
Copy Markdown
Collaborator Author

I'm currently planning to implement the e2e test only when the dynamic pane feature is live.

This update can be manually explored by adding a label or description literal in a generic thing page when there isn't one yet. The label and description in the overview then update live.
This use case could be implemented already now as an e2e test, but it's a bit contrived, so I would prefer not to.

@jg10-mastodon-social jg10-mastodon-social marked this pull request as ready for review March 21, 2026 07:12
Copy link
Copy Markdown
Contributor

@angelo-v angelo-v left a comment

Choose a reason for hiding this comment

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

I am not fully through with the review, but I left some comments you can alreay work on and perhaps answer some of my questions. I try to come back to it asap (maybe monday)

Overall this is good work and a valuable addition, even if some rework is still needed

*/
observeLabel() {
return this.observeAnyValue(
"http://www.w3.org/2006/vcard/ns#fn",
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.

suggestion: do not duplicate the label predicates, but extract them to a constant that is used both by label() and observeLabel


const currentValue = this.anyValue(...predicateUris);
const dirty$ =
typeof currentValue === "undefined"
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.

question: Why not just currentValue === undefined?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

That is indeed simpler! I somehow learnt to use typeof 😅

*/
observeDescription() {
return this.observeAnyValue(
"http://purl.org/dc/terms/description",
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.

suggestion: same as for label


receiveResource = (resource: Thing) => {
this.resource = resource;
resource
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.

issue: I think we should cancel the old subscription when receiving a new resource. You did so in pos-switch. Is there a reason you are not doing it here, or did you just forget?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think I was just working on them in parallel and hadn't yet thought to apply that pattern here.

const observable = thing.observeLabel();
observable.subscribe(subscriber);

expect(subscriber.mock.lastCall).toEqual([thing.label()]);
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.

I think this is reasonable

expect(subscriber).toHaveBeenCalledTimes(2);
});

it("pushes a value if none was present, without calling anyValue again", () => {
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.

suggestion: This test does not use the setup from before each. Split the tests that need the setup and those who dont into separate nested describe blocks

),
startWith(this.anyValue(...predicateUris)),
);
}
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.

question: Why is it important that anyValue is not called? Is it that expensive?

return merge(dirty$, addition$).pipe(
debounceTime(250),
map((value) =>
typeof value === "string" ? value : this.anyValue(...predicateUris),
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.

typeof value === "string"

issue: this check really confused me. It is basically a dirty way to check whether the observed value comes from addtions

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.

thought: I am still having trouble to fully understand the function and the role of the dirty$ observable. Perhaps you can explain a bit. I need to finish my review for now, but will come back to this. If you find the time to explain a bit in the meantime this is much appreciated. My gut feeling is that the whole function could and should be simplified.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes, that's a switch between calling anyValue and using the value from additions

I'm still getting my head around rxjs and its functionality but the intention is that addition is only read when the current value of anyValue is dirty, i.e. it hasn't been set yet or there has been a removal.

We could also simplify and just trigger for every new addition (there's already a debounce to handle concurrent removals). This would cause the label to change, i.e. a different anyValue would be selected just because there's more than one defined.
The current behaviour is intended to guarantee that once anyValue has been found, additional values in the data will be ignored.

});
});

describe("observeAnyValue", () => {
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.

thought: I think there are not enough tests given the complexity of the function. What about having multiple different (matching) predicates that "come and go" over time. How does the value change or stay consistent?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I did try to cover a lot of behaviours but I admit I didn't try to lay out all possible combinations of possible input states.

The existing tests specifically describe the behaviour I was trying to achieve, which could perhaps be simplified at the expense of some user surprise and/or marginal but unknown performance impact.

angelo-v and others added 2 commits April 10, 2026 13:06
Co-authored-by: Angelo Veltens <angelo+github.com@veltens.org>
@angelo-v angelo-v self-requested a review April 13, 2026 18:29
});

// TODO make this green
it.skip("emits new value after add-remove-add cycle", () => {
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.

@jg10-mastodon-social this actually highlights a bug and supports my gut feeling that test coverage is not high enough

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.

Reactive pos-label and pos-description

2 participants