Skip to content

refactor(trading-strategies): add explicit setState API for nested strategy state - #1175

Open
bennycode wants to merge 2 commits into
mainfrom
refactor/strategy-set-state
Open

refactor(trading-strategies): add explicit setState API for nested strategy state#1175
bennycode wants to merge 2 commits into
mainfrom
refactor/strategy-set-state

Conversation

@bennycode

Copy link
Copy Markdown
Owner

Summary

Strategy persists state via an ES Proxy whose set trap only fires on top-level property assignments. Nested mutations (e.g. this.state.protected.foo = x) change memory but never trigger onSave() — the change is silently lost on restart. ProtectedStrategy worked around this by manually reassigning the whole nested protected key through the proxy.

This PR makes the supported path explicit:

  • New Strategy.setState(patch): shallow-merges a partial state patch into the current state and fires onSave() exactly once. Writes go to the raw proxy target instead of through the Proxy, so a multi-key patch persists once rather than once per key. Typed per-call as setState<T extends Record<string, unknown>>(patch: Partial<T>), matching the existing getProxiedState<T>() pattern.
  • ProtectedStrategy refactored to route #setProtectedState through setState, removing the fragile manual proxy-reassignment pattern. Behavior is identical (including the restoreState propagation into the proxied state).
  • Class-level JSDoc on Strategy documenting the nested-mutation trap and pointing to setState as the supported path.

Top-level proxy behavior is unchanged; no public API is broken.

Test plan

  • New colocated Strategy.test.ts:
    • setState merges the patch, the result is visible via the state getter and the proxied view, and onSave fires exactly once (also for multi-key patches)
    • a nested mutation without setState does not fire onSave (documents the trap)
    • top-level assignments on the proxied state still fire onSave (existing behavior preserved)
    • restoreState round-trips a state written via setState
  • npm test in packages/trading-strategies: 23 files / 253 tests pass (includes the full ProtectedStrategy suite, unchanged)
  • npm run lint in packages/trading-strategies: clean

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces an explicit Strategy.setState() API to make nested strategy state updates persist reliably despite the existing Proxy only trapping top-level assignments. It also refactors ProtectedStrategy to use setState() for protected-state updates and adds new unit coverage documenting both the supported path and the Proxy “nested mutation” trap.

Changes:

  • Added Strategy.setState(patch) for shallow state patching with a single persistence call.
  • Refactored ProtectedStrategy.#setProtectedState() to route updates through setState().
  • Added Strategy.test.ts covering setState, Proxy trap behavior, and restoreState round-tripping.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
packages/trading-strategies/src/strategy/Strategy.ts Adds setState() and class-level documentation describing the Proxy persistence behavior.
packages/trading-strategies/src/strategy/Strategy.test.ts Adds unit tests for setState, Proxy trap behavior, and restore behavior.
packages/trading-strategies/src/strategy-protected/ProtectedStrategy.ts Updates protected-state persistence to use setState() and updates related comments.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +99 to +103
setState<T extends Record<string, unknown>>(patch: Partial<T>): void {
const target = this.#stateTarget ?? {...this.state};
Object.assign(target, patch);
this.state = {...target};
}
Comment on lines +7 to +11
* State persistence is snapshot-based: the Proxy behind `getProxiedState()` only traps
* TOP-LEVEL assignments, so mutating a nested object in place (e.g.
* `this.state.foo.bar = x`) changes memory but is silently lost on restart. Use
* {@link Strategy.setState} to update nested state — it merges the patch and persists
* in one step.
Comment thread packages/trading-strategies/src/strategy/Strategy.test.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

A couple of small but concrete issues remain in the new Strategy docs and state-sync logic (linking/accuracy and an own-property check) that should be addressed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

packages/trading-strategies/src/strategy/Strategy.ts:98

  • The setState doc says writes go to the raw Proxy target instead of through the Proxy, but that’s only true when the strategy was constructed with {state} (i.e., when #stateTarget exists). If no proxied state exists, setState just updates the snapshot. Consider adjusting the comment to reflect the conditional behavior to avoid misleading API docs.
  /**
   * The supported way to update nested state (see the class doc for the Proxy trap this
   * avoids). Writes go to the raw target instead of through the Proxy so a multi-key
   * patch persists once, not once per key.
   */
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines +6 to +11
/**
* State persistence is snapshot-based: the Proxy behind `getProxiedState()` only traps
* TOP-LEVEL assignments, so mutating a nested object reached through it in place (e.g.
* `this.getProxiedState().foo.bar = x`) changes memory but is silently lost on restart.
* Use {@link Strategy.setState} to update nested state — it merges the patch and
* persists in one step.
Comment on lines +111 to +115
for (const key of Object.keys(this.#stateTarget)) {
if (!(key in snapshot)) {
delete this.#stateTarget[key];
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants