Skip to content

[refactor!]: removed protected abortController + _handleDestory methods…#82

Open
js2me wants to merge 1 commit into
masterfrom
refactor/remove-abort-controller
Open

[refactor!]: removed protected abortController + _handleDestory methods…#82
js2me wants to merge 1 commit into
masterfrom
refactor/remove-abort-controller

Conversation

@js2me
Copy link
Copy Markdown
Owner

@js2me js2me commented May 6, 2026

… in entities


Summary by cubic

Refactored the destroy/abort lifecycle across queries and mutations to remove the internal abortController, use _abortSignal/_destroyed, and standardize on destroy() for cleanup. This is a breaking change for subclass implementers and simplifies runtime dependencies.

  • Migration

    • Replace handleDestroy() overrides with destroy() and call super.destroy() first.
    • Use this._abortSignal (may be undefined) and this._destroyed instead of abortController.
    • Call makeObservable(this) in subclass constructors if you rely on MobX annotations.
    • For mutations, if you depend on an abort signal, pass an external AbortSignal when creating the instance; otherwise a temporary one is used internally.
  • Dependencies

    • Moved linked-abort-controller to devDependencies in mobx-tanstack-query.

Written for commit 5eab613. Summary will update on new commits.

Summary by CodeRabbit

  • Breaking Changes
    • Refactored internal lifecycle management in core query and mutation classes
    • Custom subclasses must override destroy() with super.destroy() call instead of previous lifecycle method
    • Explicit MobX setup now required in subclasses where needed

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

This PR refactors the destroyable lifecycle in mobx-tanstack-query, removing the LinkedAbortController dependency and replacing it with direct AbortSignal handling. The protected handleDestroy() hook is replaced by a public destroy() method pattern across Query, InfiniteQuery, and Mutation classes.

Changes

Destroyable Lifecycle Refactor

Layer / File(s) Summary
Data Shape
src/utils/destroyable.ts
Destroyable replaces protected abortController with protected _abortSignal and protected _destroyed flag. Constructor attaches an abort listener; destroy() now sets _destroyed to true rather than aborting a controller. MobX integration and Symbol.dispose removed.
Lifecycle Integration
src/base-query.ts
Destruction checks in update(), result getter, and start() switch from abortController.signal.aborted to this._destroyed or this._abortSignal.
Query Destroy Pattern
src/query.ts
protected handleDestroy() replaced with public destroy() method that calls super.destroy(), performs cleanup, and triggers onQueryDestroy hook.
InfiniteQuery Destroy Pattern
src/inifinite-query.ts
protected handleDestroy() replaced with public destroy() method that calls super.destroy() and invokes onInfiniteQueryDestroy hook.
Mutation Destroy Pattern & Temp Abort
src/mutation.ts
Introduces protected tempAc (temporary AbortController) with helper methods abortTempAc() and recreateTempAc() to manage mutation cancellation signals. New public destroy() method calls super.destroy(), aborts tempAc, and performs observer cleanup. onSettled wrapper aborts tempAc before user callback.
Dependency & Documentation
package.json, .changeset/refactor-destroyable-internals.md
linked-abort-controller moved from dependencies to devDependencies. Changeset documents breaking changes: removal of protected abortController, handleDestroy() method, and automatic makeObservable() calls in Destroyable.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • js2me/mobx-tanstack-query#74: Overlapping lifecycle refactors replacing deprecated dispose/handleDestroy patterns with unified destroy flow and changing Destroyable internals.
  • js2me/mobx-tanstack-query#80: Modifies result behavior when destroyed/aborted in src/base-query.ts, building on the same destruction-aware logic.

Poem

🐰 Signals now flow clean and free,
No more controllers tangled in the tree—
_destroyed and _abortSignal stand tall,
The lifecycle refactor conquers all!
With destroy() calls that chain just right,
Our rabbit's code shines crystal bright! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring objective: removal of protected abortController and handleDestroy methods across the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/remove-abort-controller

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/base-query.ts (1)

585-601: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Possible hang in start() when no abortSignal is supplied and the query is destroyed mid-flight.

Previously, signal was the (always-defined) abortController.signal and was aborted by the old handleDestroy(), which would reject the when(...) in-flight. After this refactor, destroy() only sets this._destroyed = true and never aborts any signal — and this._abortSignal is undefined whenever the caller didn't pass one in config.abortSignal. In that case, calling destroy() while result.isFetching is true leaves this when waiting indefinitely (or until isFetching becomes false on its own), so the start() promise never settles.

Consider re-checking _destroyed inside the predicate so destruction unblocks the wait:

🛡️ Proposed fix
     if (this.result.isFetching) {
-      await when(() => !this.result.isFetching, {
+      await when(() => this._destroyed || !this.result.isFetching, {
         signal: this._abortSignal,
       });
+      if (this._destroyed) {
+        return this.result;
+      }
       const throwableError = this.getCurrentThrowableError();

Alternatively, manage an internal AbortController inside Destroyable.destroy() and pass its signal here so when is reliably cancelled regardless of the consumer-provided signal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/base-query.ts` around lines 585 - 601, The wait in start() can hang if
destroy() is called without an abort signal because _abortSignal may be
undefined; update the when() predicate used in start() (the call that currently
awaits when(() => !this.result.isFetching, { signal: this._abortSignal })) to
also check this._destroyed (e.g., predicate returns !this.result.isFetching ||
this._destroyed) so destruction unblocks the wait, and after the wait re-check
this._destroyed and handle as appropriate (throw or return) to avoid leaving
start() pending; alternatively, implement an internal AbortController in
Destroyable.destroy() and expose its signal as this._abortSignal so the existing
when(...) is cancelled reliably.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mutation.ts`:
- Around line 199-208: The current tempAc lifecycle races because
recreateTempAc()/abortTempAc() mutate a single shared tempAc used by all
concurrent mutationFn calls; change to create a per-call AbortController inside
the mutationFn wrapper (capture it in a local variable or store it in a WeakMap
keyed by the mutation context/result) and pass that specific controller.signal
to the underlying mutationFn, then ensure the onSettled wrapper only aborts that
same per-call controller instead of calling the global abortTempAc(); keep the
existing behavior when this._abortSignal (config.abortSignal) is provided by
using that shared signal as-is, otherwise always create and own a fresh per-call
controller so overlapping mutations do not cancel each other (refer to
recreateTempAc, abortTempAc, mutationFn, onSettled, tempAc and this._abortSignal
to locate the changes).

In `@src/utils/destroyable.ts`:
- Around line 5-20: The constructor currently ignores pre-aborted signals and
destroy() is not idempotent; update constructor (the class constructor that
assigns this._abortSignal and this._destroyed) to immediately call
this.destroy() if this._abortSignal?.aborted is true, then still add the abort
listener as before, and change destroy() to be guarded (if (this._destroyed)
return) before setting this._destroyed = true so repeated calls (from manual
disposal, Symbol.dispose/using, or the abort listener) are safe; reference the
constructor, this._abortSignal, and destroy() so subclasses like Query,
InfiniteQuery, and Mutation rely on the idempotent super.destroy().

---

Outside diff comments:
In `@src/base-query.ts`:
- Around line 585-601: The wait in start() can hang if destroy() is called
without an abort signal because _abortSignal may be undefined; update the when()
predicate used in start() (the call that currently awaits when(() =>
!this.result.isFetching, { signal: this._abortSignal })) to also check
this._destroyed (e.g., predicate returns !this.result.isFetching ||
this._destroyed) so destruction unblocks the wait, and after the wait re-check
this._destroyed and handle as appropriate (throw or return) to avoid leaving
start() pending; alternatively, implement an internal AbortController in
Destroyable.destroy() and expose its signal as this._abortSignal so the existing
when(...) is cancelled reliably.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e576a5f-ea33-4505-b8c4-228df41435cf

📥 Commits

Reviewing files that changed from the base of the PR and between 548c918 and 5eab613.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • .changeset/refactor-destroyable-internals.md
  • package.json
  • src/base-query.ts
  • src/inifinite-query.ts
  • src/mutation.ts
  • src/query.ts
  • src/utils/destroyable.ts

Comment thread src/mutation.ts
Comment on lines +199 to 208
onSettled: (...args) => {
this.abortTempAc();
return this.mutationOptions.onSettled?.(...args);
},
mutationFn: (variables, context) =>
mutationFn?.(variables, {
...context,
signal: this.abortController.signal,
signal: this._abortSignal ?? this.recreateTempAc().signal,
} satisfies MutationFunctionContext),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Does @tanstack/query-core MutationObserver 5.90.2 support concurrent in-flight mutations from the same observer, or does mutate() supersede a previous in-flight mutation?

💡 Result:

Yes—MutationObserver (and thus @tanstack/query-core’s mutate/mutateAsync behavior) supports concurrent in-flight mutations from the same MutationObserver; calling mutate does not supersede/cancel a previous in-flight mutation. Evidence: The React TanStack Query docs state that “Per default, all mutations run in parallel - even if you invoke.mutate of the same mutation multiple times,” and that to avoid this you must use a scope.id so they run in serial [1][2]. Additionally, the core MutationObserver implementation shows that each mutate call removes the prior observer link, builds/chooses a mutation from the mutation cache, and then executes it; it doesn’t cancel the previous one, it just updates which mutation the observer is attached to [3]. So if you call observer.mutate(x) then observer.mutate(y) while the first is still pending, both mutation executions can be in-flight concurrently; they are not automatically replaced by the latest call. If you want “only one at a time” semantics, use a scope with an id to queue in serial [1][2], otherwise concurrency is the default.

Citations:


tempAc lifecycle has a cross-mutation race when calls overlap.

@tanstack/query-core's MutationObserver supports concurrent in-flight mutations by default—calling mutate() does not cancel a prior in-flight call. However, this code assumes single-flight semantics and will break under concurrent load:

  1. Overlapping mutations cancel each other's signals. Each mutationFn invocation calls recreateTempAc(), which aborts the previous tempAc. If the same Mutation instance has an in-flight call (A) and mutate() is invoked again (B) before A settles, A's signal is aborted by B's recreateTempAc() even though both are executing concurrently. Then when A eventually settles, the wrapped onSettled calls abortTempAc() and aborts B's signal — corrupting an unrelated in-flight mutation.

  2. Asymmetric signal semantics. When config.abortSignal is provided, all calls share _abortSignal and abortTempAc() becomes a no-op (no per-call cancellation). When it isn't, a fresh tempAc is created per call. The signal a consumer's mutationFn receives therefore behaves differently depending on whether abortSignal was passed in config — this is observable and surprising.

A safer approach is to track the controller per in-flight call (e.g., capture it in a closure inside mutationFn and pass that exact reference to onSettled via a WeakMap keyed on the result/context, or wrap mutationFn itself so each invocation owns its own controller and aborts only that one on settle).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mutation.ts` around lines 199 - 208, The current tempAc lifecycle races
because recreateTempAc()/abortTempAc() mutate a single shared tempAc used by all
concurrent mutationFn calls; change to create a per-call AbortController inside
the mutationFn wrapper (capture it in a local variable or store it in a WeakMap
keyed by the mutation context/result) and pass that specific controller.signal
to the underlying mutationFn, then ensure the onSettled wrapper only aborts that
same per-call controller instead of calling the global abortTempAc(); keep the
existing behavior when this._abortSignal (config.abortSignal) is provided by
using that shared signal as-is, otherwise always create and own a fresh per-call
controller so overlapping mutations do not cancel each other (refer to
recreateTempAc, abortTempAc, mutationFn, onSettled, tempAc and this._abortSignal
to locate the changes).

Comment thread src/utils/destroyable.ts
Comment on lines 5 to 20
constructor(abortSignal?: AbortSignal) {
this.abortController = new LinkedAbortController(abortSignal);

action(this, 'handleDestroy');
makeObservable(this);

this.abortController.signal.addEventListener('abort', () => {
this.handleDestroy();
});
this._abortSignal = abortSignal;
this._destroyed = false;

this._abortSignal?.addEventListener(
'abort',
() => {
this.destroy();
},
{ once: true },
);
}

destroy() {
this.abortController?.abort();
this._destroyed = true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Two lifecycle edge cases worth handling explicitly.

  1. Pre-aborted signal is silently ignored. Per the DOM spec, addEventListener('abort', ...) on an already-aborted AbortSignal does not fire. So if a consumer passes a signal that's already aborted to new Query({ abortSignal }), the instance is never marked _destroyed and never tears down.

  2. Double destroy() invocation. If destroy() is called manually (e.g. via using / Symbol.dispose, or a subclass's own teardown path) and the signal later aborts, the once: true listener still fires and re-enters destroy(). Subclasses (Query, InfiniteQuery, Mutation) re-run cleanup and re-fire onQueryDestroy / onInfiniteQueryDestroy / onMutationDestroy hooks.

🛡️ Proposed fix — guard idempotency and handle pre-aborted signals
   constructor(abortSignal?: AbortSignal) {
     this._abortSignal = abortSignal;
     this._destroyed = false;

-    this._abortSignal?.addEventListener(
-      'abort',
-      () => {
-        this.destroy();
-      },
-      { once: true },
-    );
+    if (abortSignal?.aborted) {
+      // Defer so subclass constructors finish before destroy() runs.
+      queueMicrotask(() => this.destroy());
+    } else {
+      abortSignal?.addEventListener(
+        'abort',
+        () => this.destroy(),
+        { once: true },
+      );
+    }
   }

   destroy() {
+    if (this._destroyed) return;
     this._destroyed = true;
   }

The early-return in destroy() makes subclass overrides naturally idempotent (each one starts with super.destroy()).

📝 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
constructor(abortSignal?: AbortSignal) {
this.abortController = new LinkedAbortController(abortSignal);
action(this, 'handleDestroy');
makeObservable(this);
this.abortController.signal.addEventListener('abort', () => {
this.handleDestroy();
});
this._abortSignal = abortSignal;
this._destroyed = false;
this._abortSignal?.addEventListener(
'abort',
() => {
this.destroy();
},
{ once: true },
);
}
destroy() {
this.abortController?.abort();
this._destroyed = true;
}
constructor(abortSignal?: AbortSignal) {
this._abortSignal = abortSignal;
this._destroyed = false;
if (abortSignal?.aborted) {
// Defer so subclass constructors finish before destroy() runs.
queueMicrotask(() => this.destroy());
} else {
abortSignal?.addEventListener(
'abort',
() => this.destroy(),
{ once: true },
);
}
}
destroy() {
if (this._destroyed) return;
this._destroyed = true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/destroyable.ts` around lines 5 - 20, The constructor currently
ignores pre-aborted signals and destroy() is not idempotent; update constructor
(the class constructor that assigns this._abortSignal and this._destroyed) to
immediately call this.destroy() if this._abortSignal?.aborted is true, then
still add the abort listener as before, and change destroy() to be guarded (if
(this._destroyed) return) before setting this._destroyed = true so repeated
calls (from manual disposal, Symbol.dispose/using, or the abort listener) are
safe; reference the constructor, this._abortSignal, and destroy() so subclasses
like Query, InfiniteQuery, and Mutation rely on the idempotent super.destroy().

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/base-query.ts">

<violation number="1" location="src/base-query.ts:590">
P1: `this._abortSignal` can be `undefined` when no external abort signal is provided to the constructor. This means `when()` won't be cancelled on `destroy()`, leaving a dangling observation/promise. The mutation class handles this with `this._abortSignal ?? this.recreateTempAc().signal` as a fallback.</violation>
</file>

<file name="src/mutation.ts">

<violation number="1" location="src/mutation.ts:199">
P1: Race condition: when multiple mutations are triggered concurrently (without `_abortSignal`), the options-level `onSettled` from an earlier mutation will abort `tempAc` that belongs to the later in-flight mutation, prematurely signalling abort to it. Consider tracking which `tempAc` instance the `onSettled` should abort (e.g., capture a reference at call time) rather than always aborting the current one.</violation>
</file>

<file name="src/utils/destroyable.ts">

<violation number="1" location="src/utils/destroyable.ts:9">
P2: Two lifecycle edge cases:
1. If a pre-aborted signal is passed, `addEventListener('abort', ...)` will never fire (the event was already dispatched per DOM spec), so the instance is never destroyed automatically.
2. No idempotency guard — if `destroy()` is called manually and the signal later aborts, the listener re-enters `destroy()`, causing subclass cleanup and hooks to fire twice.

Consider checking `abortSignal?.aborted` in the constructor (and deferring via `queueMicrotask` so subclass constructors finish), and adding an early `if (this._destroyed) return;` guard in `destroy()`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

Comment thread src/base-query.ts
if (this.result.isFetching) {
await when(() => !this.result.isFetching, {
signal: this.abortController.signal,
signal: this._abortSignal,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 6, 2026

Choose a reason for hiding this comment

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

P1: this._abortSignal can be undefined when no external abort signal is provided to the constructor. This means when() won't be cancelled on destroy(), leaving a dangling observation/promise. The mutation class handles this with this._abortSignal ?? this.recreateTempAc().signal as a fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/base-query.ts, line 590:

<comment>`this._abortSignal` can be `undefined` when no external abort signal is provided to the constructor. This means `when()` won't be cancelled on `destroy()`, leaving a dangling observation/promise. The mutation class handles this with `this._abortSignal ?? this.recreateTempAc().signal` as a fallback.</comment>

<file context>
@@ -587,7 +587,7 @@ export abstract class BaseQuery<
     if (this.result.isFetching) {
       await when(() => !this.result.isFetching, {
-        signal: this.abortController.signal,
+        signal: this._abortSignal,
       });
       const throwableError = this.getCurrentThrowableError();
</file context>
Fix with Cubic

Comment thread src/mutation.ts
// @ts-expect-error
>(queryClient, {
...this.mutationOptions,
onSettled: (...args) => {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 6, 2026

Choose a reason for hiding this comment

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

P1: Race condition: when multiple mutations are triggered concurrently (without _abortSignal), the options-level onSettled from an earlier mutation will abort tempAc that belongs to the later in-flight mutation, prematurely signalling abort to it. Consider tracking which tempAc instance the onSettled should abort (e.g., capture a reference at call time) rather than always aborting the current one.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/mutation.ts, line 199:

<comment>Race condition: when multiple mutations are triggered concurrently (without `_abortSignal`), the options-level `onSettled` from an earlier mutation will abort `tempAc` that belongs to the later in-flight mutation, prematurely signalling abort to it. Consider tracking which `tempAc` instance the `onSettled` should abort (e.g., capture a reference at call time) rather than always aborting the current one.</comment>

<file context>
@@ -193,10 +196,14 @@ export class Mutation<
       // @ts-expect-error
     >(queryClient, {
       ...this.mutationOptions,
+      onSettled: (...args) => {
+        this.abortTempAc();
+        return this.mutationOptions.onSettled?.(...args);
</file context>
Fix with Cubic

Comment thread src/utils/destroyable.ts
this._abortSignal = abortSignal;
this._destroyed = false;

this._abortSignal?.addEventListener(
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot May 6, 2026

Choose a reason for hiding this comment

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

P2: Two lifecycle edge cases:

  1. If a pre-aborted signal is passed, addEventListener('abort', ...) will never fire (the event was already dispatched per DOM spec), so the instance is never destroyed automatically.
  2. No idempotency guard — if destroy() is called manually and the signal later aborts, the listener re-enters destroy(), causing subclass cleanup and hooks to fire twice.

Consider checking abortSignal?.aborted in the constructor (and deferring via queueMicrotask so subclass constructors finish), and adding an early if (this._destroyed) return; guard in destroy().

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/utils/destroyable.ts, line 9:

<comment>Two lifecycle edge cases:
1. If a pre-aborted signal is passed, `addEventListener('abort', ...)` will never fire (the event was already dispatched per DOM spec), so the instance is never destroyed automatically.
2. No idempotency guard — if `destroy()` is called manually and the signal later aborts, the listener re-enters `destroy()`, causing subclass cleanup and hooks to fire twice.

Consider checking `abortSignal?.aborted` in the constructor (and deferring via `queueMicrotask` so subclass constructors finish), and adding an early `if (this._destroyed) return;` guard in `destroy()`.</comment>

<file context>
@@ -1,26 +1,24 @@
+    this._abortSignal = abortSignal;
+    this._destroyed = false;
+
+    this._abortSignal?.addEventListener(
+      'abort',
+      () => {
</file context>
Fix with Cubic

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.

1 participant