universal-api-layer 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ // src/core/errors.ts
2
+ var ApiError = class extends Error {
3
+ constructor(message, status = 0, fieldErrors = {}, data, code = "http", cause) {
4
+ super(message, cause === void 0 ? void 0 : { cause });
5
+ this.status = status;
6
+ this.fieldErrors = fieldErrors;
7
+ this.data = data;
8
+ this.code = code;
9
+ this.name = "ApiError";
10
+ }
11
+ status;
12
+ fieldErrors;
13
+ data;
14
+ code;
15
+ };
16
+
17
+ export {
18
+ ApiError
19
+ };
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ declare function templates(framework: string, language: string): Record<string, string>;
3
+
4
+ export { templates };
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { readFile, writeFile, mkdir, access } from "fs/promises";
5
+ import { resolve } from "path";
6
+ import { createInterface } from "readline/promises";
7
+ function templates(framework, language) {
8
+ if (framework === "angular") {
9
+ if (language !== "typescript")
10
+ throw new Error("Angular scaffolding requires TypeScript.");
11
+ return {
12
+ "src/api.providers.ts": `import {createApiClient} from 'universal-api-layer';
13
+ import {provideApiClient} from 'universal-api-layer/angular';
14
+ export const apiProviders=provideApiClient(()=>createApiClient({baseUrl:'/api'}));
15
+ `,
16
+ "src/ApiExample.ts": `import {Component,DestroyRef,inject,signal} from '@angular/core';
17
+ import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
18
+ import {ApiService} from 'universal-api-layer/angular';
19
+ @Component({selector:'api-example',standalone:true,template:'<button (click)="load()">Load users</button><pre>{{ data() }}</pre><p role="alert">{{ error() }}</p>'})
20
+ export class ApiExample {private api=inject(ApiService);private destroy=inject(DestroyRef);readonly data=signal('');readonly error=signal('');load(){this.api.request$('/users').pipe(takeUntilDestroyed(this.destroy)).subscribe({next:value=>this.data.set(JSON.stringify(value)),error:cause=>this.error.set(cause instanceof Error?cause.message:'Request failed')});}}
21
+ `
22
+ };
23
+ }
24
+ const ts = language === "typescript";
25
+ const ext = ts ? "ts" : "js";
26
+ const setup = `import { createApiClient } from 'universal-api-layer';
27
+ export const client = createApiClient({ baseUrl: '/api' });
28
+ `;
29
+ const type = ts ? "<{ data: { id: number; name: string }[] }, { id: number; name: string }[]>" : "";
30
+ if (framework === "react")
31
+ return {
32
+ [`src/api.${ext}`]: setup,
33
+ [`src/ApiExample.${ts ? "tsx" : "jsx"}`]: `import { useApiQuery, useApiMutation } from 'universal-api-layer/react';
34
+ export default function ApiExample() {
35
+ const users = useApiQuery${type}({ key: ['users'], url: '/users', select: response => response?.data ?? [] });
36
+ const create = useApiMutation({ url: '/users', invalidateKeys: [['users']] });
37
+ return <section><button onClick={() => create.mutate({ name: 'Bahaa' })}>Add user</button>{users.isPending ? <p>Loading...</p> : users.error ? <p>{users.error.message}</p> : <pre>{JSON.stringify(users.data)}</pre>}{create.error && <pre>{JSON.stringify(create.error.fieldErrors)}</pre>}</section>;
38
+ }
39
+ `
40
+ };
41
+ return {
42
+ [`src/api.${ext}`]: setup,
43
+ "src/ApiExample.vue": `<script setup${ts ? ' lang="ts"' : ""}>
44
+ import { useApiQuery, useApiMutation } from 'universal-api-layer/vue';
45
+ const { data, isPending, error } = useApiQuery${type}({ key: ['users'], url: '/users', select: response => response?.data ?? [] });
46
+ const { mutate, error: mutationError } = useApiMutation({ url: '/users', invalidateKeys: [['users']] });
47
+ </script>
48
+ <template><section><button @click="mutate({ name: 'Bahaa' })">Add user</button><p v-if="isPending">Loading...</p><p v-else-if="error">{{ error.message }}</p><pre v-else>{{ data }}</pre><pre v-if="mutationError">{{ mutationError.fieldErrors }}</pre></section></template>
49
+ `
50
+ };
51
+ }
52
+ async function main() {
53
+ const args = process.argv.slice(2);
54
+ if (args.includes("--help")) {
55
+ console.log(
56
+ "universal-api-layer init [--framework react|vue|angular] [--language javascript|typescript] [--force]"
57
+ );
58
+ return;
59
+ }
60
+ if (args.shift() !== "init")
61
+ throw new Error(
62
+ "Use: universal-api-layer init --framework react|vue|angular --language javascript|typescript"
63
+ );
64
+ const flags = {};
65
+ let force = false;
66
+ while (args.length) {
67
+ const arg = args.shift();
68
+ if (arg === "--force") {
69
+ force = true;
70
+ continue;
71
+ }
72
+ if (!["--framework", "--language"].includes(arg) || !args[0] || args[0].startsWith("--"))
73
+ throw new Error(`Invalid option ${arg}. Use --help.`);
74
+ flags[arg.slice(2)] = args.shift();
75
+ }
76
+ if (flags.framework && !["react", "vue", "angular"].includes(flags.framework))
77
+ throw new Error("Expected --framework react|vue|angular.");
78
+ if (flags.language && !["javascript", "typescript"].includes(flags.language))
79
+ throw new Error("Expected --language javascript|typescript.");
80
+ let pkg = {};
81
+ try {
82
+ pkg = JSON.parse(await readFile("package.json", "utf8"));
83
+ } catch (error) {
84
+ if (error.code !== "ENOENT") throw error;
85
+ }
86
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
87
+ const frameworks = ["react", "vue", "angular"].filter(
88
+ (name) => deps[name === "angular" ? "@angular/core" : name]
89
+ );
90
+ const exists = async (name) => access(name).then(
91
+ () => true,
92
+ () => false
93
+ );
94
+ let framework = flags.framework ?? (frameworks.length === 1 ? frameworks[0] : void 0);
95
+ const hasTsConfig = await exists("tsconfig.json");
96
+ const hasJsConfig = await exists("jsconfig.json");
97
+ let language = flags.language ?? (hasTsConfig !== hasJsConfig ? hasTsConfig ? "typescript" : "javascript" : void 0);
98
+ if (framework === "angular") language = flags.language ?? "typescript";
99
+ if (!language && !(hasTsConfig && hasJsConfig)) {
100
+ const { readdir } = await import("fs/promises");
101
+ const ignored = /* @__PURE__ */ new Set([
102
+ "node_modules",
103
+ "dist",
104
+ "build",
105
+ "coverage",
106
+ ".git",
107
+ ".next",
108
+ ".nuxt",
109
+ ".output",
110
+ ".artifacts",
111
+ "tests",
112
+ "test"
113
+ ]);
114
+ async function sourceFiles(directory) {
115
+ const entries = await readdir(directory, { withFileTypes: true });
116
+ const children = await Promise.all(
117
+ entries.filter(
118
+ (entry) => !ignored.has(entry.name) && !entry.name.startsWith(".")
119
+ ).map(async (entry) => {
120
+ const name = resolve(directory, entry.name);
121
+ if (entry.isDirectory()) return sourceFiles(name);
122
+ return entry.isFile() && /\.(?:[cm]?[jt]sx?|vue)$/.test(entry.name) && !/\.(?:config|test|spec|d)\.[cm]?[jt]sx?$/.test(entry.name) ? [name] : [];
123
+ })
124
+ );
125
+ return children.flat();
126
+ }
127
+ const files2 = await sourceFiles(await exists("src") ? "src" : ".");
128
+ let ts = files2.some((f) => /\.(ts|tsx|mts|cts)$/.test(f));
129
+ let js = files2.some((f) => /\.(js|jsx|mjs|cjs)$/.test(f));
130
+ for (const name of files2.filter((f) => f.endsWith(".vue"))) {
131
+ const source = await readFile(name, "utf8");
132
+ if (/<script\b[^>]*\blang\s*=\s*['"]ts['"]/i.test(source)) ts = true;
133
+ else js = true;
134
+ }
135
+ if (ts !== js) language = ts ? "typescript" : "javascript";
136
+ }
137
+ async function choose(label, choices) {
138
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
139
+ throw new Error(
140
+ `Cannot detect ${label}. Pass --${label} ${choices.join("|")} in non-interactive mode.`
141
+ );
142
+ const input = createInterface({
143
+ input: process.stdin,
144
+ output: process.stdout
145
+ });
146
+ try {
147
+ return (await input.question(`${label} (${choices.join("/")}): `)).trim();
148
+ } finally {
149
+ input.close();
150
+ }
151
+ }
152
+ framework ??= await choose("framework", ["react", "vue", "angular"]);
153
+ if (framework === "angular") {
154
+ if (flags.language === "javascript")
155
+ throw new Error("Angular scaffolding requires TypeScript.");
156
+ language = "typescript";
157
+ }
158
+ language ??= await choose("language", ["javascript", "typescript"]);
159
+ if (!["react", "vue", "angular"].includes(framework) || !["javascript", "typescript"].includes(language))
160
+ throw new Error(
161
+ "Expected --framework react|vue|angular and --language javascript|typescript."
162
+ );
163
+ const files = templates(framework, language);
164
+ if (!force) {
165
+ for (const name of Object.keys(files))
166
+ if (await exists(name))
167
+ throw new Error(
168
+ `Refusing to overwrite ${name}. Use --force to replace generated files.`
169
+ );
170
+ }
171
+ for (const [name, content] of Object.entries(files)) {
172
+ await mkdir(resolve(name, ".."), { recursive: true });
173
+ await writeFile(name, content, { flag: force ? "w" : "wx" });
174
+ }
175
+ if (framework === "angular") {
176
+ console.log(
177
+ "Created Angular API providers and standalone example. Add apiProviders from './api.providers' to your application configuration providers, then import ApiExample into a standalone component."
178
+ );
179
+ console.log(
180
+ "Install: npm install universal-api-layer @angular/core rxjs@^7.8.2\nUse Angular 20.3-22 and its matching Angular/Node/TypeScript toolchain. No existing bootstrap files were changed."
181
+ );
182
+ return;
183
+ }
184
+ console.log(
185
+ `Created API setup and example.
186
+ Install: npm install universal-api-layer ${framework === "react" ? "react@^19 react-dom@^19 @tanstack/react-query@^5" : "vue@^3.5 @tanstack/vue-query@^5"}`
187
+ );
188
+ console.log(
189
+ "Requires Node >=22.13 and npm >=11.21.1; install the listed optional peers explicitly."
190
+ );
191
+ console.log(
192
+ framework === "react" ? "Import { client } from './api'; import { ApiProvider } from 'universal-api-layer/react'; wrap <App /> with <ApiProvider client={client}>. ApiProvider installs QueryClientProvider; pass queryClient to reuse an existing instance." : "Import { client } from './api'; import { createApiPlugin } from 'universal-api-layer/vue'; call app.use(createApiPlugin({ client })) before app.mount()."
193
+ );
194
+ if (deps.next || deps.nuxt)
195
+ console.log(
196
+ "Next.js/Nuxt detected: only standalone API/example files were generated; register them manually. Next.js requires a client provider boundary; Nuxt requires a client plugin and a per-request client for SSR. Configure these manually."
197
+ );
198
+ }
199
+ main().catch((error) => {
200
+ console.error(error.message);
201
+ process.exitCode = 1;
202
+ });
203
+ export {
204
+ templates
205
+ };
@@ -0,0 +1,169 @@
1
+ type FieldErrors = Record<string, string[]>;
2
+ type ApiErrorCode = "backpressure" | "http" | "network" | "timeout" | "cancelled" | "auth" | "parse" | "configuration" | "callback" | "validation";
3
+ declare class ApiError extends Error {
4
+ status: number;
5
+ fieldErrors: FieldErrors;
6
+ data?: unknown | undefined;
7
+ code: ApiErrorCode;
8
+ constructor(message: string, status?: number, fieldErrors?: FieldErrors, data?: unknown | undefined, code?: ApiErrorCode, cause?: unknown);
9
+ }
10
+
11
+ interface ApiSession {
12
+ userId: string;
13
+ token?: string | null;
14
+ tenantId?: string | null;
15
+ }
16
+ interface SessionSnapshot {
17
+ readonly userId: string | null;
18
+ readonly tenantId: string | null;
19
+ readonly revision: number;
20
+ }
21
+
22
+ /** Structural Standard Schema v1 support: Zod, Valibot and other compatible schemas need no runtime dependency here. */
23
+ interface StandardSchema<T> {
24
+ readonly "~standard": {
25
+ readonly version: 1;
26
+ readonly vendor: string;
27
+ readonly validate: (value: unknown) => {
28
+ readonly value: T;
29
+ readonly issues?: undefined;
30
+ } | {
31
+ readonly issues: readonly {
32
+ readonly message: string;
33
+ }[];
34
+ } | Promise<{
35
+ readonly value: T;
36
+ readonly issues?: undefined;
37
+ } | {
38
+ readonly issues: readonly {
39
+ readonly message: string;
40
+ }[];
41
+ }>;
42
+ };
43
+ }
44
+ type ResponseSchema<T> = ((value: unknown) => T | Promise<T>) | {
45
+ parse(value: unknown): T;
46
+ parseAsync?: (value: unknown) => Promise<T>;
47
+ } | StandardSchema<T>;
48
+
49
+ type Params = Record<string, string | number | boolean | null | undefined | readonly (string | number | boolean)[]>;
50
+ /** Uses each platform's native Fetch types without requiring DOM libs in Node projects. */
51
+ type ApiHeaders = NonNullable<RequestInit["headers"]>;
52
+ interface ApiResponse<T = unknown> {
53
+ readonly data: T | null;
54
+ readonly headers: Headers;
55
+ readonly status: number;
56
+ readonly statusText: string;
57
+ readonly url: string;
58
+ }
59
+ interface RequestOptions<T = unknown> {
60
+ /** Keep body-only results by default; full mode also exposes response metadata. */
61
+ responseMode?: "data" | "full";
62
+ schema?: ResponseSchema<T> | false;
63
+ authRetry?: boolean;
64
+ requiresAuth?: boolean;
65
+ method?: string;
66
+ params?: Params;
67
+ body?: unknown;
68
+ headers?: ApiHeaders;
69
+ signal?: AbortSignal;
70
+ timeoutMs?: number;
71
+ }
72
+ interface StreamOptions extends Omit<RequestOptions, "responseMode" | "schema"> {
73
+ /** text yields decoded UTF-8 chunks; sse yields complete SSE events. */
74
+ format?: "text" | "sse";
75
+ /** Maximum buffered SSE line/event length in UTF-16 code units. Default 1 MiB. */
76
+ maxEventSize?: number;
77
+ }
78
+ interface ServerSentEvent {
79
+ readonly event: string;
80
+ readonly data: string;
81
+ readonly id: string;
82
+ /** Server retry hint only; this client never reconnects automatically. */
83
+ readonly retry?: number;
84
+ }
85
+ interface ClientOptions {
86
+ baseUrl?: string;
87
+ session?: ApiSession | null;
88
+ /** Return a new access token. Shared across concurrent 401s and retried once. */
89
+ refreshToken?: (signal: AbortSignal, session: SessionSnapshot) => Promise<string>;
90
+ refreshTimeoutMs?: number;
91
+ schema?: ResponseSchema<unknown>;
92
+ getToken?: (signal: AbortSignal) => string | null | undefined | Promise<string | null | undefined>;
93
+ headers?: ApiHeaders;
94
+ timeoutMs?: number;
95
+ fetch?: typeof fetch;
96
+ credentials?: RequestInit["credentials"];
97
+ normalizeErrors?: (data: unknown) => FieldErrors;
98
+ /** Non-secret stable identity for intentional cache sharing/SSR hydration. Unique per client by default. */
99
+ cacheScope?: string;
100
+ /** Additional HTTP(S) origins allowed to receive this client's requests and credentials. */
101
+ allowedOrigins?: readonly string[];
102
+ }
103
+ interface QueryConfig<T, S = T | null> {
104
+ schema?: ResponseSchema<T> | false;
105
+ requiresAuth?: boolean;
106
+ authRetry?: boolean;
107
+ key: readonly unknown[];
108
+ url: string;
109
+ params?: Params;
110
+ headers?: ApiHeaders;
111
+ /** Non-secret account/tenant identity; change on authentication transitions. */
112
+ cacheScope?: string;
113
+ timeoutMs?: number;
114
+ enabled?: boolean;
115
+ staleTime?: number;
116
+ retry?: boolean | number;
117
+ select?: (response: T | null) => S;
118
+ }
119
+ interface MutationConfig<T, V, S = T | null> {
120
+ schema?: ResponseSchema<T> | false;
121
+ requiresAuth?: boolean;
122
+ authRetry?: boolean;
123
+ url: string | ((variables: V) => string);
124
+ method?: string;
125
+ mapPayload?: (variables: V) => unknown;
126
+ headers?: ApiHeaders;
127
+ timeoutMs?: number;
128
+ signal?: AbortSignal;
129
+ select?: (response: T | null) => S;
130
+ invalidateKeys?: readonly (readonly unknown[])[];
131
+ /** all explicitly includes matching native TanStack queries in this QueryClient. */
132
+ invalidateScope?: "client" | "all";
133
+ onSuccess?: (data: S, variables: V) => unknown | Promise<unknown>;
134
+ }
135
+
136
+ declare const DEFAULT_TIMEOUT_MS = 15000;
137
+ declare function createApiClient(config?: ClientOptions): Readonly<{
138
+ cacheScope: string;
139
+ queryKey: (key: readonly unknown[], url: string, params?: Params, headers?: ApiHeaders, scope?: string) => unknown[];
140
+ setSession: (value: ApiSession | null) => void;
141
+ logout: () => void;
142
+ getSessionSnapshot: () => SessionSnapshot;
143
+ subscribeSession: (listener: () => void) => () => void;
144
+ assertSession: (revision: number) => void;
145
+ withSession<T>(operation: () => T | Promise<T>, revision?: number): Promise<T>;
146
+ ownsQuery(key: readonly unknown[]): boolean;
147
+ request: {
148
+ <T = unknown>(url: string, request: RequestOptions<T> & {
149
+ responseMode: "full";
150
+ }): Promise<ApiResponse<T>>;
151
+ <T = unknown>(url: string, request?: RequestOptions<T> & {
152
+ responseMode?: "data";
153
+ }): Promise<T | null>;
154
+ <T = unknown>(url: string, request: RequestOptions<T>): Promise<T | null | ApiResponse<T>>;
155
+ };
156
+ stream: {
157
+ (url: string, request: StreamOptions & {
158
+ format: "sse";
159
+ }): AsyncGenerator<ServerSentEvent, void, unknown>;
160
+ (url: string, request?: StreamOptions & {
161
+ format?: "text";
162
+ }): AsyncGenerator<string, void, unknown>;
163
+ (url: string, request: StreamOptions): AsyncGenerator<string | ServerSentEvent, void, unknown>;
164
+ };
165
+ }>;
166
+ type ApiClient = ReturnType<typeof createApiClient>;
167
+
168
+ export { DEFAULT_TIMEOUT_MS as D, ApiError as f, createApiClient as j };
169
+ export type { ApiHeaders as A, ClientOptions as C, FieldErrors as F, MutationConfig as M, Params as P, QueryConfig as Q, RequestOptions as R, SessionSnapshot as S, ApiSession as a, ApiResponse as b, StreamOptions as c, ServerSentEvent as d, ApiClient as e, ApiErrorCode as g, ResponseSchema as h, StandardSchema as i };
@@ -0,0 +1,13 @@
1
+ import { f as ApiError, F as FieldErrors, e as ApiClient, P as Params, A as ApiHeaders, M as MutationConfig } from '../client-ByBlZaRg.js';
2
+ export { g as ApiErrorCode, b as ApiResponse, a as ApiSession, C as ClientOptions, D as DEFAULT_TIMEOUT_MS, Q as QueryConfig, R as RequestOptions, h as ResponseSchema, d as ServerSentEvent, S as SessionSnapshot, i as StandardSchema, c as StreamOptions, j as createApiClient } from '../client-ByBlZaRg.js';
3
+
4
+ declare function invokeCallback<T>(callback: () => T): T;
5
+ declare function invokeAsyncCallback(callback: () => unknown | Promise<unknown>): Promise<void>;
6
+
7
+ declare function laravelErrors(data: unknown): FieldErrors;
8
+ declare function queryIdentity(client: ApiClient, key: readonly unknown[], url: string, params?: Params, headers?: ApiHeaders, scope?: string): unknown[];
9
+ declare function runMutation<T, V, S = T | null>(client: ApiClient, config: MutationConfig<T, V, S>, variables: V): Promise<S>;
10
+ /** Avoid retries for deterministic schema/auth/client failures; honor adapter or QueryClient overrides. */
11
+ declare function defaultQueryRetry(failureCount: number, error: ApiError): boolean;
12
+
13
+ export { ApiClient, ApiError, ApiHeaders, FieldErrors, MutationConfig, Params, defaultQueryRetry, invokeAsyncCallback, invokeCallback, laravelErrors, queryIdentity, runMutation };
@@ -0,0 +1,26 @@
1
+ import {
2
+ DEFAULT_TIMEOUT_MS,
3
+ createApiClient,
4
+ defaultQueryRetry,
5
+ laravelErrors,
6
+ queryIdentity,
7
+ runMutation
8
+ } from "../chunk-HDW7BOZI.js";
9
+ import {
10
+ invokeAsyncCallback,
11
+ invokeCallback
12
+ } from "../chunk-DXYWZQW5.js";
13
+ import {
14
+ ApiError
15
+ } from "../chunk-YTM3FV65.js";
16
+ export {
17
+ ApiError,
18
+ DEFAULT_TIMEOUT_MS,
19
+ createApiClient,
20
+ defaultQueryRetry,
21
+ invokeAsyncCallback,
22
+ invokeCallback,
23
+ laravelErrors,
24
+ queryIdentity,
25
+ runMutation
26
+ };
@@ -0,0 +1,16 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { QueryClient, UseMutationResult, UseQueryResult } from '@tanstack/react-query';
4
+ import { ApiClient, MutationConfig, ApiError, QueryConfig, SessionSnapshot } from '../core/index.js';
5
+
6
+ declare function ApiProvider({ client, queryClient, children, }: {
7
+ client: ApiClient;
8
+ queryClient?: QueryClient;
9
+ children: ReactNode;
10
+ }): react.JSX.Element;
11
+ declare function useApiClient(): ApiClient;
12
+ declare function useApiSession(): SessionSnapshot;
13
+ declare function useApiQuery<T = unknown, S = T | null>(config: QueryConfig<T, S>): UseQueryResult<S, ApiError>;
14
+ declare function useApiMutation<T = unknown, V = unknown, S = T | null>(config: MutationConfig<T, V, S>): UseMutationResult<S, ApiError, V, unknown>;
15
+
16
+ export { ApiProvider, useApiClient, useApiMutation, useApiQuery, useApiSession };
@@ -0,0 +1,227 @@
1
+ import {
2
+ defaultQueryRetry,
3
+ queryIdentity,
4
+ runMutation
5
+ } from "../chunk-HDW7BOZI.js";
6
+ import {
7
+ invokeAsyncCallback,
8
+ invokeCallback
9
+ } from "../chunk-DXYWZQW5.js";
10
+ import "../chunk-YTM3FV65.js";
11
+
12
+ // src/react/index.tsx
13
+ import {
14
+ createContext,
15
+ useContext,
16
+ useEffect,
17
+ useMemo,
18
+ useState,
19
+ useSyncExternalStore
20
+ } from "react";
21
+ import {
22
+ QueryClient,
23
+ QueryClientProvider,
24
+ useQuery,
25
+ useMutation,
26
+ useQueryClient
27
+ } from "@tanstack/react-query";
28
+ import { jsx } from "react/jsx-runtime";
29
+ var Context = createContext(null);
30
+ function clearClientCache(client, cache, staleOnly = false) {
31
+ const current = client.getSessionSnapshot();
32
+ const predicate = (query) => {
33
+ if (!client.ownsQuery(query.queryKey)) return false;
34
+ if (!staleOnly) return true;
35
+ const snapshot = query.queryKey.at(-1).session;
36
+ return !snapshot || snapshot.revision !== current.revision || snapshot.userId !== current.userId || snapshot.tenantId !== current.tenantId;
37
+ };
38
+ void cache.cancelQueries({ predicate }).catch(() => {
39
+ });
40
+ cache.removeQueries({ predicate });
41
+ for (const mutation of cache.getMutationCache().getAll()) {
42
+ const variables = mutation.state.variables;
43
+ const revision = variables && typeof variables === "object" && "revision" in variables ? variables.revision : void 0;
44
+ if (mutation.options.meta?.universalApiLayer === client.cacheScope && (!staleOnly || revision !== current.revision))
45
+ cache.getMutationCache().remove(mutation);
46
+ }
47
+ }
48
+ var cacheBindings = /* @__PURE__ */ new WeakMap();
49
+ function bindCache(client, cache) {
50
+ let caches = cacheBindings.get(client);
51
+ if (!caches) {
52
+ caches = /* @__PURE__ */ new WeakMap();
53
+ cacheBindings.set(client, caches);
54
+ }
55
+ let binding = caches.get(cache);
56
+ if (!binding) {
57
+ clearClientCache(client, cache, true);
58
+ binding = {
59
+ count: 0,
60
+ unsubscribe: client.subscribeSession(
61
+ () => clearClientCache(client, cache)
62
+ )
63
+ };
64
+ caches.set(cache, binding);
65
+ }
66
+ binding.count++;
67
+ const current = binding;
68
+ return () => {
69
+ current.count--;
70
+ if (current.count === 0) {
71
+ current.unsubscribe();
72
+ clearClientCache(client, cache);
73
+ caches.delete(cache);
74
+ }
75
+ };
76
+ }
77
+ function ApiProvider({
78
+ client,
79
+ queryClient,
80
+ children
81
+ }) {
82
+ const [owned] = useState(() => queryClient ?? new QueryClient());
83
+ const cache = queryClient ?? owned;
84
+ const session = useSyncExternalStore(
85
+ client.subscribeSession,
86
+ client.getSessionSnapshot,
87
+ client.getSessionSnapshot
88
+ );
89
+ useEffect(() => bindCache(client, cache), [client, cache]);
90
+ const value = useMemo(() => ({ client, session }), [client, session]);
91
+ return /* @__PURE__ */ jsx(Context.Provider, { value, children: /* @__PURE__ */ jsx(QueryClientProvider, { client: cache, children }) });
92
+ }
93
+ function useContextValue() {
94
+ const value = useContext(Context);
95
+ if (!value) throw new Error("Wrap your application in ApiProvider");
96
+ return value;
97
+ }
98
+ function useApiClient() {
99
+ return useContextValue().client;
100
+ }
101
+ function useApiSession() {
102
+ return useContextValue().session;
103
+ }
104
+ function useApiQuery(config) {
105
+ const { client, session } = useContextValue();
106
+ const cache = useQueryClient();
107
+ return useQuery({
108
+ queryKey: queryIdentity(
109
+ client,
110
+ config.key,
111
+ config.url,
112
+ config.params,
113
+ config.headers,
114
+ config.cacheScope
115
+ ),
116
+ queryFn: ({ signal }) => {
117
+ client.assertSession(session.revision);
118
+ return client.request(config.url, {
119
+ params: config.params,
120
+ headers: config.headers,
121
+ timeoutMs: config.timeoutMs,
122
+ signal,
123
+ schema: config.schema,
124
+ authRetry: config.authRetry,
125
+ requiresAuth: config.requiresAuth
126
+ });
127
+ },
128
+ select: config.select ? (data) => invokeCallback(() => config.select(data)) : void 0,
129
+ enabled: config.requiresAuth ? !!session.userId && config.enabled !== false : config.enabled,
130
+ staleTime: config.staleTime,
131
+ retry: config.retry ?? cache.getDefaultOptions().queries?.retry ?? defaultQueryRetry
132
+ });
133
+ }
134
+ function useApiMutation(config) {
135
+ const { client, session } = useContextValue();
136
+ const cache = useQueryClient();
137
+ const mutation = useMutation({
138
+ mutationKey: ["universal-api-layer", client.cacheScope, session.revision],
139
+ meta: { universalApiLayer: client.cacheScope },
140
+ mutationFn: async (envelope) => {
141
+ client.assertSession(envelope.revision);
142
+ const data = await runMutation(client, config, envelope.input);
143
+ client.assertSession(envelope.revision);
144
+ return data;
145
+ },
146
+ onSuccess: async (data, envelope) => {
147
+ client.assertSession(envelope.revision);
148
+ await Promise.all(
149
+ (config.invalidateKeys ?? []).map(
150
+ (queryKey) => cache.invalidateQueries({
151
+ queryKey,
152
+ predicate: config.invalidateScope === "all" ? void 0 : (query) => client.ownsQuery(query.queryKey)
153
+ })
154
+ )
155
+ );
156
+ client.assertSession(envelope.revision);
157
+ await invokeAsyncCallback(() => config.onSuccess?.(data, envelope.input));
158
+ client.assertSession(envelope.revision);
159
+ }
160
+ });
161
+ const translate = (options) => options ? {
162
+ onSuccess: (data, envelope, context, mutationContext) => {
163
+ if (envelope.revision === client.getSessionSnapshot().revision)
164
+ options.onSuccess?.(
165
+ data,
166
+ envelope.input,
167
+ context,
168
+ mutationContext
169
+ );
170
+ },
171
+ onError: (error, envelope, context, mutationContext) => {
172
+ if (envelope.revision === client.getSessionSnapshot().revision)
173
+ options.onError?.(
174
+ error,
175
+ envelope.input,
176
+ context,
177
+ mutationContext
178
+ );
179
+ },
180
+ onSettled: (data, error, envelope, context, mutationContext) => {
181
+ if (envelope.revision === client.getSessionSnapshot().revision)
182
+ options.onSettled?.(
183
+ data,
184
+ error,
185
+ envelope.input,
186
+ context,
187
+ mutationContext
188
+ );
189
+ }
190
+ } : void 0;
191
+ const current = !mutation.variables || mutation.variables.revision === session.revision;
192
+ return {
193
+ ...mutation,
194
+ ...!current ? {
195
+ data: void 0,
196
+ error: null,
197
+ context: void 0,
198
+ status: "idle",
199
+ isIdle: true,
200
+ isPending: false,
201
+ isSuccess: false,
202
+ isError: false,
203
+ isPaused: false,
204
+ submittedAt: 0,
205
+ failureCount: 0,
206
+ failureReason: null
207
+ } : {},
208
+ variables: current ? mutation.variables?.input : void 0,
209
+ mutate: (input, options) => mutation.mutate(
210
+ { input, revision: client.getSessionSnapshot().revision },
211
+ translate(options)
212
+ ),
213
+ mutateAsync: (input, options) => client.withSession(
214
+ () => mutation.mutateAsync(
215
+ { input, revision: client.getSessionSnapshot().revision },
216
+ translate(options)
217
+ )
218
+ )
219
+ };
220
+ }
221
+ export {
222
+ ApiProvider,
223
+ useApiClient,
224
+ useApiMutation,
225
+ useApiQuery,
226
+ useApiSession
227
+ };