From c6bed45372e0a6c99d1b78d8bf4103ce192a61b6 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Wed, 10 Jun 2026 21:14:18 +0500 Subject: [PATCH] fix(client): remove frozen ObjectStackClient singleton (server-switch split-brain) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `objectStackClient` was a module-level client built once at import with the initial API_URL. `ObjectStackClient` snapshots `baseUrl` at construction, so it stayed pinned to the startup host and would silently talk to the wrong server after the user connected elsewhere — a split-brain landmine. It had no real consumers (only a "is defined" test); every live call site uses the React-provider client (recreated on server/token change) or the live-URL helpers (`apiFetch`/`resolveApiUrl`, which read API_URL at call time). Remove the const; keep `getObjectStackClient()` (builds fresh against the current URL) as the documented non-React primitive, with a note on why there is no singleton. Add a regression test asserting it always reflects the latest `setObjectStackApiUrl` across a switch. Also fixes a latent tsc error in the P2 OptionBadge test (`type: "status"` is not a valid FieldType literal → `"select"`). Co-Authored-By: Claude Opus 4.8 (1M context) --- __tests__/components/OptionBadge.test.tsx | 2 +- __tests__/lib/objectstack.test.ts | 25 +++++++++++++++++++---- lib/objectstack.ts | 23 +++++++++++---------- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/__tests__/components/OptionBadge.test.tsx b/__tests__/components/OptionBadge.test.tsx index ef7c120..2ae9573 100644 --- a/__tests__/components/OptionBadge.test.tsx +++ b/__tests__/components/OptionBadge.test.tsx @@ -14,7 +14,7 @@ import type { FieldDefinition } from "~/components/renderers/types"; describe("OptionBadge", () => { const statusField: FieldDefinition = { name: "status", - type: "status", + type: "select", options: [ { value: "completed", label: "Completed", color: "#10B981" }, { value: "blocked", label: "Blocked", color: "red" }, diff --git a/__tests__/lib/objectstack.test.ts b/__tests__/lib/objectstack.test.ts index a8e18f3..c5e7b96 100644 --- a/__tests__/lib/objectstack.test.ts +++ b/__tests__/lib/objectstack.test.ts @@ -2,7 +2,11 @@ * Tests for objectstack — client factory */ import { ObjectStackClient } from "@objectstack/client"; -import { createObjectStackClient, objectStackClient } from "~/lib/objectstack"; +import { + createObjectStackClient, + getObjectStackClient, + setObjectStackApiUrl, +} from "~/lib/objectstack"; describe("createObjectStackClient", () => { it("creates a client with token", () => { @@ -20,8 +24,21 @@ describe("createObjectStackClient", () => { }); }); -describe("objectStackClient", () => { - it("is defined", () => { - expect(objectStackClient).toBeDefined(); +describe("getObjectStackClient", () => { + // Guards against the split-brain bug a frozen module-level singleton caused: + // it must always build against the *currently-configured* URL so a server + // switch can't leave a client talking to the old host. + it("always builds against the current API URL after a server switch", () => { + setObjectStackApiUrl("https://server-a.example"); + getObjectStackClient(); + expect(ObjectStackClient).toHaveBeenLastCalledWith( + expect.objectContaining({ baseUrl: "https://server-a.example" }), + ); + + setObjectStackApiUrl("https://server-b.example"); + getObjectStackClient(); + expect(ObjectStackClient).toHaveBeenLastCalledWith( + expect.objectContaining({ baseUrl: "https://server-b.example" }), + ); }); }); diff --git a/lib/objectstack.ts b/lib/objectstack.ts index ce338a7..b8db2a6 100644 --- a/lib/objectstack.ts +++ b/lib/objectstack.ts @@ -164,8 +164,18 @@ export function apiFetch(pathOrUrl: string, init?: RequestInit): Promise