onlyworlds_obsidian-plugin/client.ts
Titus 87e003e2e7 2.0.0: SDK, auto-sync, native settings, modernized build
Major rebuild. Plugin now uses @onlyworlds/sdk for API calls via Obsidian's
requestUrl, native PluginSettingTab for configuration, and a debounced auto-sync
engine that pushes element edits to onlyworlds.com after idle.

Changes
- Drop handrolled API code; route all writes through @onlyworlds/sdk subclass
- Native settings tab: API key, PIN, default world, sync mode, debounce
- PIN handling: persist in settings or cache per session; no per-save prompts
- Auto-sync toggle with configurable debounce (default 3s)
- Status bar + ribbon icon reflect sync state (idle/dirty/syncing/synced/error)
- Ribbon click opens menu (Sync / Settings / About) instead of firing sync
- Settings API key wins over per-world World.md (legacy fallback preserved)
- Add Map, Pin, Marker to element category enum (now 22)
- ESLint config (eslint-plugin-obsidianmd) scoped to new code
- Modernize build: TS 5.8, esbuild 0.25, stricter tsconfig
- Drop dead code: GraphViewExtensions, dead command imports, handleApiError

Compatibility
- Existing v1 vaults work unchanged. Span-tag format preserved.
- OnlyWorlds/Settings.md still readable; new settings tab takes precedence.
- isDesktopOnly: true. Mobile pending verification.
2026-05-11 12:51:28 +02:00

86 lines
2.4 KiB
TypeScript

import { OnlyWorldsClient, OnlyWorldsConfig } from "@onlyworlds/sdk";
import { requestUrl, RequestUrlParam } from "obsidian";
/**
* OnlyWorlds SDK client subclass that routes HTTP through Obsidian's requestUrl().
*
* The base SDK uses native fetch() which Obsidian plugin reviewers flag (CORS,
* mobile compatibility). This subclass overrides request() to delegate to requestUrl
* while preserving the SDK's resource accessors, _ids/_id rewriting, and error handling.
*/
export class ObsidianOnlyWorldsClient extends OnlyWorldsClient {
private obsidianBaseUrl: string;
private obsidianHeaders: Record<string, string>;
constructor(config: OnlyWorldsConfig) {
super(config);
this.obsidianBaseUrl = config.baseUrl || "https://www.onlyworlds.com/api/worldapi";
this.obsidianHeaders = {
"Content-Type": "application/json",
"API-Key": config.apiKey,
"API-Pin": config.apiPin,
};
}
async request<T>(
method: string,
path: string,
options?: {
params?: Record<string, unknown>;
body?: unknown;
}
): Promise<T> {
const url = new URL(`${this.obsidianBaseUrl}${path}`);
if (options?.params) {
for (const [key, value] of Object.entries(options.params)) {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
}
}
const req: RequestUrlParam = {
url: url.toString(),
method,
headers: this.obsidianHeaders,
throw: false,
};
if (options?.body && ["POST", "PATCH", "PUT"].includes(method)) {
req.body = JSON.stringify(options.body);
}
const response = await requestUrl(req);
if (response.status >= 400) {
let errorMessage = `API Error ${response.status}`;
const text = response.text;
if (text) {
try {
const errJson = JSON.parse(text);
if (Array.isArray(errJson.detail)) {
const validationErrors = errJson.detail
.map((err: { loc?: unknown[]; msg?: string }) => {
const location = Array.isArray(err.loc) ? err.loc.join(".") : "unknown";
return `${location}: ${err.msg ?? ""}`;
})
.join("; ");
errorMessage += `: ${validationErrors}`;
} else {
errorMessage += `: ${errJson.detail ?? errJson.error ?? text}`;
}
} catch {
errorMessage += `: ${text}`;
}
}
throw new Error(errorMessage);
}
if (response.status === 204) {
return undefined as unknown as T;
}
return response.json as T;
}
}