Skip to content

RangeError: Maximum call stack size exceeded when updating a many-to-many relation #372

Description

@michael-wolfenden

Description

Removing an item from a many() relation array using .splice() inside an update() callback throws RangeError: Maximum call stack size exceeded. This happens when the related entity itself has a self-referential many() relation defined on it (in the reproduction, users has a followedBy: many(users) relation), even though the crash occurs on an unrelated collection (articles).

Reproduction

StackBlitz: https://stackblitz.com/edit/vitejs-vite-l3gsewlc?file=src%2Fmain.ts&terminal=dev

import { Collection } from '@msw/data';
import * as z from 'zod';

const userSchema = z.object({
  id: z.number(),
  get followedBy() {
    return z.array(userSchema);
  },
});

const articleSchema = z.object({
  id: z.number(),
  get favouritedBy() {
    return z.array(userSchema);
  },
});

const users = new Collection({ schema: userSchema });
const articles = new Collection({ schema: articleSchema });

users.defineRelations(({ many }) => ({
  followedBy: many(users),
}));

articles.defineRelations(({ many }) => ({
  favouritedBy: many(users),
}));

// john and jane both follow each other
const johndoe = await users.create({ id: 1, followedBy: [] });
const janesmith = await users.create({ id: 2, followedBy: [] });

await users.update(johndoe, {
  data(user) {
    user.followedBy.push(janesmith);
  },
});

await users.update(janesmith, {
  data(user) {
    user.followedBy.push(johndoe);
  },
});

const article = await articles.create({ id: 1, favouritedBy: [janesmith] });

// Uncaught RangeError: Maximum call stack size exceeded
await articles.update(article, {
  data(article) {
    article.favouritedBy.splice(0, 1);
  },
});

Expected behavior

janesmith should be removed from article.favouritedBy without error.

Actual behavior

Throws:

RangeError: Maximum call stack size exceeded
    at Array.filter (<anonymous>)
    at Query.<anonymous> (collection-CpEWi2De.mjs:102:22)
    at Query.test (collection-CpEWi2De.mjs:66:17)
    at #query (collection-CpEWi2De.mjs:765:32)
    at #query.next (<anonymous>)
    at Array.from (<anonymous>)
    at Collection.findMany (collection-CpEWi2De.mjs:548:25)
    at collection-CpEWi2De.mjs:416:29
    at Array.flatMap (<anonymous>)
    at Many.resolve (collection-CpEWi2De.mjs:415:34)

This suggests an infinite recursion between Many.resolveCollection.findManyQuery.test, likely triggered by the self-referential followedBy relation on users being re-resolved every time favouritedBy is queried/mutated, with no cycle guard.

Additional notes

  • Removing the self-referential followedBy: many(users) relation (or not populating it with mutual references) avoids the crash.
  • The crash happens on articles.update, a collection that doesn't itself have a circular relation — the cycle appears to come from resolving the related users records.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions