Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion __tests__/components/OptionBadge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
25 changes: 21 additions & 4 deletions __tests__/lib/objectstack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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" }),
);
});
});
23 changes: 12 additions & 11 deletions lib/objectstack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,18 @@ export function apiFetch(pathOrUrl: string, init?: RequestInit): Promise<Respons
}

/**
* Get a singleton client for unauthenticated/discovery requests.
* For authenticated requests, use the provider which creates a token-aware client.
* Build a client for unauthenticated/discovery requests against the
* **currently-configured** server. Returns a fresh instance each call so it
* always reflects the latest {@link setObjectStackApiUrl} — never hold the
* result across a server switch, or it will talk to the old host.
*
* For authenticated requests, prefer the React provider's client (recreated on
* server/token change); this is for non-React call sites.
*
* NOTE: there is deliberately no module-level singleton. `ObjectStackClient`
* snapshots `baseUrl` at construction, so a long-lived instance created at
* import time would freeze on the initial URL and silently talk to the wrong
* server after the user connects elsewhere (a split-brain bug).
*/
export function getObjectStackClient(): ObjectStackClient {
return new ObjectStackClient({
Expand All @@ -174,12 +184,3 @@ export function getObjectStackClient(): ObjectStackClient {
logger: clientLogger,
});
}

/**
* Legacy singleton — prefer `getObjectStackClient()` for current URL.
*/
export const objectStackClient = new ObjectStackClient({
baseUrl: API_URL,
fetch: authAwareFetch,
logger: clientLogger,
});
Loading