commit 1c3c618c27d183961f402b8cbf367558b935d80f Author: Fabian @ Blax Software Date: Tue Apr 7 08:44:00 2026 +0200 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e728995 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +*.tgz diff --git a/README.md b/README.md new file mode 100644 index 0000000..e83f787 --- /dev/null +++ b/README.md @@ -0,0 +1,210 @@ +# @blax-software/networking + +Framework-agnostic API and WebSocket client library with optional Vue, Nuxt, and axios adapters. + +## Features + +- **Zero required dependencies** — core uses native `fetch` and `WebSocket` +- **Tree-shakeable** — import only what you need +- **SSR-safe** — server detection with automatic no-op stubs +- **Framework adapters** — Vue `ref()` reactivity, Nuxt auto-config, axios compatibility +- **TypeScript-first** — Full type coverage with `.d.ts` exports + +## Install + +```bash +npm install @blax-software/networking + +# Optional peer dependencies: +npm install vue # for /vue and /nuxt entry points +npm install axios # for /axios adapter +``` + +## Quick Start + +### Vanilla / React / Any framework + +```typescript +import { createApiClient, createWsClient } from '@blax-software/networking' + +const api = createApiClient({ + serverUrl: 'https://api.example.com', +}) + +const ws = createWsClient({ + url: 'wss://api.example.com/app/ws', + getAuthToken: () => api.getBearer(), +}) + +// REST +const users = await api.get('users') + +// WebSocket +await ws.connect() +ws.setAppReady() +const result = await ws.send('user.profile', { id: 123 }) + +// Listen for events — returns an unsubscribe function +const off = ws.listen('chat.message', null, (data) => { + console.log('New message:', data) +}) +// Call off() to stop listening (works with React useEffect cleanup, etc.) +``` + +### Vue 3 + +```typescript +import { useApiClient, useWsClient, useWsListener } from '@blax-software/networking/vue' + +const api = useApiClient({ + serverUrl: 'https://api.example.com', +}) + +const ws = useWsClient({ + url: 'wss://api.example.com/app/ws', + getAuthToken: () => api.getBearer(), +}) + +// ws.is_setup, ws.is_opened etc. are Vue refs +watch(ws.is_setup, (ready) => { + if (ready) console.log('WebSocket ready') +}) + +// Auto-cleanup on component unmount +useWsListener(ws, 'notifications.new', null, (data) => { + console.log('Notification:', data) +}) +``` + +### Nuxt 3 + +```typescript +// plugins/networking.client.ts +import { createFromNuxtConfig } from '@blax-software/networking/nuxt' + +export default defineNuxtPlugin(() => { + const { api, ws } = createFromNuxtConfig() + + // Optional: store bearer, connect WS, etc. + api.setBearer(localStorage.getItem('bearerToken') ?? '') + ws.connect() + ws.setAppReady() + + return { provide: { api, ws } } +}) +``` + +Reads these keys from `useRuntimeConfig().public`: + +| Key | Description | +|-----------------------|-------------------------------| +| `SERVER_URL` | REST API base URL | +| `SERVER_URL_INTERNAL` | Internal URL for SSR requests | +| `WEBS_URL` | WebSocket hostname | +| `WS_PROTOCOL` | `'wss'` or `'ws'` | + +### With axios + +```typescript +import { createApiClient } from '@blax-software/networking' +import { createAxiosAdapter } from '@blax-software/networking/axios' +import axios from 'axios' + +const axiosInstance = axios.create({ + baseURL: 'https://api.example.com', + withCredentials: true, +}) + +const api = createApiClient({ + serverUrl: 'https://api.example.com', + http: createAxiosAdapter(axiosInstance), +}) +``` + +## API Reference + +### `createApiClient(config: ApiClientConfig): ApiClient` + +| Config | Type | Default | Description | +|-------------------|----------------------------|------------------|----------------------------------------| +| `serverUrl` | `string \| () => string` | **required** | Base URL for HTTP requests | +| `ssrServerUrl` | `string \| () => string` | — | Alternate URL for server-side requests | +| `http` | `HttpAdapter` | `fetchAdapter` | HTTP adapter (fetch, axios, custom) | +| `notify` | `NotifyFn` | — | Notification callback for errors | +| `storage` | `StorageAdapter` | `browserStorage` | Token persistence adapter | +| `storageKey` | `string` | `'bearerToken'` | Storage key for the bearer token | +| `timeout` | `number` | `10000` | Request timeout in ms | +| `withCredentials` | `boolean` | `true` | Include cookies in requests | +| `retryOn503` | `boolean` | `true` | Auto-retry on HTTP 503 | +| `apiPrefix` | `string` | `'api/'` | Prefix prepended to relative paths | +| `isServer` | `boolean \| () => boolean` | `false` | Whether running on the server (SSR) | + +**ApiClient methods:** + +| Method | Returns | Description | +|-------------------------------|-------------------------|----------------------------------------| +| `get(path, params?)` | `Promise` | GET request | +| `post(path, data?, params?)` | `Promise` | POST request | +| `put(path, data?, params?)` | `Promise` | PUT request | +| `delete(path, params?)` | `Promise` | DELETE request | +| `patch(path, data?, params?)` | `Promise` | PATCH request | +| `csrf(path?)` | `Promise` | Fetch CSRF cookie | +| `setBearer(token)` | `void` | Set auth token | +| `getBearer()` | `string \| null` | Get current auth token | +| `clearBearer()` | `void` | Remove auth token | +| `getServerUrl()` | `string` | Resolve current server URL | +| `parseError(error)` | `never` | Extract error, notify, and re-throw | +| `parseThen(response, msg?)` | `any` | Show success notification, return data | +| `configure(partial)` | `void` | Update config at runtime | + +### `createWsClient(config: WsClientConfig, createRef?): WsClient` + +| Config | Type | Default | Description | +|---------------------|----------------------------|---------------|--------------------------------------------| +| `url` | `string \| () => string` | **required** | Full WebSocket URL | +| `getAuthToken` | `() => string \| null` | — | Auth token getter for channel subscription | +| `notify` | `NotifyFn` | — | Connection state notifications | +| `translate` | `TranslateFn` | — | Translation function for notification text | +| `defaultChannel` | `string` | `'websocket'` | Default channel name | +| `heartbeatInterval` | `number` | `20000` | Ping interval (ms) | +| `reconnectDelay` | `number` | `3000` | Delay before reconnect attempt (ms) | +| `autoReconnect` | `boolean` | `true` | Auto-reconnect on disconnect | +| `isServer` | `boolean \| () => boolean` | `false` | Returns safe no-op stub when true | +| `isNativePlatform` | `boolean \| () => boolean` | `false` | Suppresses browser-only notifications | + +**WsClient methods:** + +| Method | Returns | Description | +|-------------------------------------------|------------------------------|------------------------------------------| +| `connect(force?)` | `Promise` | Open socket connection | +| `send(event, data?, channel?, progress?)` | `Promise` | Send event, await response | +| `listen(event, channel, callback)` | `() => void` | Listen for event, returns unsubscribe fn | +| `listenOnce(event, channel?)` | `Promise` | Resolve on next occurrence | +| `setAppReady()` | `void` | Unblock gated `send()` calls | +| `resetConnection()` | `void` | Force channels to re-subscribe | +| `destroy()` | `void` | Close and clean up everything | + +**Reactive state (Vue refs when using vue/nuxt adapters, plain objects otherwise):** + +| Property | Type | Description | +|----------------------------|------------------------|-----------------------------| +| `is_opened` | `ReactiveRef` | Socket is open | +| `is_setup` | `ReactiveRef` | Default channel established | +| `is_connecting_socket` | `ReactiveRef` | Connection in progress | +| `is_after_lost_connection` | `ReactiveRef` | Had a connection loss | + +## Storage Adapters + +```typescript +import { browserStorage, memoryStorage } from '@blax-software/networking' + +// Default — uses localStorage (safe for SSR, returns null on errors) +const api1 = createApiClient({ serverUrl: '...', storage: browserStorage }) + +// In-memory — for SSR, tests, or environments without localStorage +const api2 = createApiClient({ serverUrl: '...', storage: memoryStorage() }) +``` + +## License + +MIT diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d87ecac --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2063 @@ +{ + "name": "@blax-software/networking", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@blax-software/networking", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "axios": "^1.7.0", + "tsup": "^8.0.0", + "typescript": "^5.5.0", + "vue": "^3.5.0" + }, + "peerDependencies": { + "axios": ">=1.0.0", + "vue": ">=3.3.0" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "vue": "3.5.32" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..57b030a --- /dev/null +++ b/package.json @@ -0,0 +1,73 @@ +{ + "name": "@blax-software/networking", + "version": "0.1.0", + "description": "Plug-and-play API + WebSocket client. Framework-agnostic core with optional Vue, Nuxt, and React bindings.", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./vue": { + "types": "./dist/vue.d.ts", + "import": "./dist/vue.js", + "require": "./dist/vue.cjs" + }, + "./nuxt": { + "types": "./dist/nuxt.d.ts", + "import": "./dist/nuxt.js", + "require": "./dist/nuxt.cjs" + }, + "./axios": { + "types": "./dist/api-axios.d.ts", + "import": "./dist/api-axios.js", + "require": "./dist/api-axios.cjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "peerDependencies": { + "axios": ">=1.0.0", + "vue": ">=3.3.0" + }, + "peerDependenciesMeta": { + "axios": { + "optional": true + }, + "vue": { + "optional": true + } + }, + "devDependencies": { + "axios": "^1.7.0", + "tsup": "^8.0.0", + "typescript": "^5.5.0", + "vue": "^3.5.0" + }, + "keywords": [ + "websocket", + "api-client", + "http", + "fetch", + "vue", + "nuxt", + "react", + "realtime" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/blax-software/npm-networking.git" + } +} \ No newline at end of file diff --git a/src/api-axios.ts b/src/api-axios.ts new file mode 100644 index 0000000..df2b953 --- /dev/null +++ b/src/api-axios.ts @@ -0,0 +1,38 @@ +import type { HttpAdapter, HttpResponse, HttpRequestConfig } from './types' + +/** + * Creates an HTTP adapter backed by an axios instance. + * + * @example + * ```ts + * import axios from 'axios' + * import { createApiClient } from '@blax-software/networking' + * import { createAxiosAdapter } from '@blax-software/networking/axios' + * + * const api = createApiClient({ + * serverUrl: 'https://api.example.com', + * http: createAxiosAdapter(axios.create({ withCredentials: true })), + * }) + * ``` + */ +export function createAxiosAdapter(axiosInstance: any): HttpAdapter { + return { + async request(config: HttpRequestConfig): Promise> { + const res = await axiosInstance.request({ + method: config.method, + url: config.url, + data: config.data, + headers: config.headers, + params: config.params, + timeout: config.timeout, + withCredentials: config.withCredentials, + }) + + return { + data: res.data, + status: res.status, + headers: res.headers ?? {}, + } + }, + } +} diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..c0d0f3e --- /dev/null +++ b/src/api.ts @@ -0,0 +1,362 @@ +import type { + ApiClientConfig, + HttpAdapter, + HttpResponse, + HttpRequestConfig, + NotifyFn, + StorageAdapter, +} from './types' +import { browserStorage, memoryStorage } from './types' + +// --------------------------------------------------------------------------- +// Built-in fetch adapter +// --------------------------------------------------------------------------- + +function serializeParams(params: any, prefix = ''): string { + const parts: string[] = [] + for (const key in params) { + if (!Object.prototype.hasOwnProperty.call(params, key)) continue + const value = params[key] + if (value === undefined || value === null) continue + const newKey = prefix + ? `${prefix}[${encodeURIComponent(key)}]` + : encodeURIComponent(key) + if (typeof value === 'object' && !Array.isArray(value)) { + parts.push(serializeParams(value, newKey)) + } else { + parts.push(`${newKey}=${encodeURIComponent(value)}`) + } + } + return parts.filter(Boolean).join('&') +} + +export const fetchAdapter: HttpAdapter = { + async request(config: HttpRequestConfig): Promise> { + let url = config.url + if (config.params) { + const qs = serializeParams(config.params) + if (qs) url += (url.includes('?') ? '&' : '?') + qs + } + + const headers: Record = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + ...(config.headers ?? {}), + } + + const init: RequestInit = { + method: config.method, + headers, + credentials: config.withCredentials ? 'include' : 'same-origin', + } + + // Timeout via AbortSignal when available + if (config.timeout && typeof AbortSignal !== 'undefined' && 'timeout' in AbortSignal) { + init.signal = AbortSignal.timeout(config.timeout) + } + + if (config.data !== undefined && config.method !== 'GET') { + if (typeof FormData !== 'undefined' && config.data instanceof FormData) { + init.body = config.data + // Let the browser set Content-Type with boundary + delete (init.headers as Record)['Content-Type'] + } else { + init.body = JSON.stringify(config.data) + } + } + + const res = await fetch(url, init) + const contentType = res.headers.get('content-type') ?? '' + let data: any + if (contentType.includes('application/json')) { + data = await res.json() + } else { + data = await res.text() + } + + if (!res.ok) { + const error: any = new Error(data?.message ?? `HTTP ${res.status}`) + error.response = { data, status: res.status, headers: {} } + error.status = res.status + throw error + } + + const responseHeaders: Record = {} + res.headers.forEach((v, k) => { responseHeaders[k] = v }) + + return { data, status: res.status, headers: responseHeaders } + }, +} + +// --------------------------------------------------------------------------- +// API Client +// --------------------------------------------------------------------------- + +export class ApiClient { + private _config: Required< + Pick + > & + ApiClientConfig + private _http: HttpAdapter + private _storage: StorageAdapter + private _notify: NotifyFn | undefined + private _bearerToken: string = '' + + /** + * When true, `parseError` throws without showing notifications. + * Useful during app bootstrap to suppress transient errors. + */ + silentErrors = false + + constructor(config: ApiClientConfig) { + this._config = { + timeout: 10000, + withCredentials: true, + apiPrefix: 'api/', + storageKey: 'bearerToken', + retryOn503: true, + maxRetries: 2, + ...config, + } + this._http = config.http ?? fetchAdapter + this._storage = config.storage ?? (this._isServer() ? memoryStorage() : browserStorage) + this._notify = config.notify + } + + // ------------------------------------------------------------------------- + // Configuration + // ------------------------------------------------------------------------- + + /** Update configuration at runtime. */ + configure(partial: Partial): void { + Object.assign(this._config, partial) + if (partial.http) this._http = partial.http + if (partial.storage) this._storage = partial.storage + if (partial.notify !== undefined) this._notify = partial.notify + } + + private _isServer(): boolean { + const s = this._config.isServer + return typeof s === 'function' ? s() : (s ?? false) + } + + // ------------------------------------------------------------------------- + // URL helpers + // ------------------------------------------------------------------------- + + /** Resolve the current backend base URL (trailing slash guaranteed). */ + getBackendUrl(): string { + const getter = + this._isServer() && this._config.ssrServerUrl + ? this._config.ssrServerUrl + : this._config.serverUrl + + let url = typeof getter === 'function' ? getter() : getter + if (!url) throw new Error('[networking] serverUrl is not configured') + if (!url.endsWith('/')) url += '/' + return url + } + + /** + * Build a full URL for a client asset served from a warehouse endpoint. + * Override in subclass or configure a custom path if your backend differs. + */ + clientAsset(path: string): string { + return this.getBackendUrl() + 'warehouse/' + path + '?clientasset=true' + } + + /** + * Normalize a URL path: + * - Leading `/` is stripped (treated as absolute relative to backend root). + * - Otherwise, `apiPrefix` is prepended if absent. + */ + cleanseUrl(url: string): string { + if (url.startsWith('/')) return url.substring(1) + if (!url.startsWith(this._config.apiPrefix!)) return this._config.apiPrefix + url + return url + } + + // ------------------------------------------------------------------------- + // Auth + // ------------------------------------------------------------------------- + + get bearerToken(): string { + return this._bearerToken + } + + setBearer(token: string | null): void { + if (!token) { + this._bearerToken = '' + if (!this._isServer()) { + this._storage.remove(this._config.storageKey!) + } + return + } + this._bearerToken = token + if (!this._isServer()) { + this._storage.set(this._config.storageKey!, token) + } + } + + /** Read the bearer token from storage and activate it. Returns the token or null. */ + loadBearerFromStorage(): string | null { + if (this._isServer()) return null + const token = this._storage.get(this._config.storageKey!) + if (token) this._bearerToken = token + return token + } + + // ------------------------------------------------------------------------- + // Internal request plumbing + // ------------------------------------------------------------------------- + + private _buildHeaders(overrides?: Record): Record { + const base: Record = { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + } + if (this._bearerToken) { + base['Authorization'] = `Bearer ${this._bearerToken}` + } + + const defaults = this._config.defaultHeaders + if (defaults) { + Object.assign(base, typeof defaults === 'function' ? defaults() : defaults) + } + + if (overrides) Object.assign(base, overrides) + return base + } + + private async _request(config: HttpRequestConfig, attempt = 0): Promise> { + try { + return await this._http.request(config) + } catch (error: any) { + if ( + this._config.retryOn503 && + error?.status === 503 && + attempt < this._config.maxRetries + ) { + const backoffMs = (attempt + 1) * 1000 + await new Promise((r) => setTimeout(r, backoffMs)) + return this._request(config, attempt + 1) + } + throw error + } + } + + // ------------------------------------------------------------------------- + // HTTP verbs + // ------------------------------------------------------------------------- + + async get(url: string, params?: any): Promise> { + return this._request({ + method: 'GET', + url: this.getBackendUrl() + this.cleanseUrl(url), + params, + headers: this._buildHeaders(), + timeout: this._config.timeout, + withCredentials: this._config.withCredentials, + }) + } + + async post(url: string, data?: any, headers?: Record): Promise> { + return this._request({ + method: 'POST', + url: this.getBackendUrl() + this.cleanseUrl(url), + data, + headers: this._buildHeaders(headers), + timeout: this._config.timeout, + withCredentials: this._config.withCredentials, + }) + } + + async put(url: string, data?: any): Promise> { + return this._request({ + method: 'PUT', + url: this.getBackendUrl() + this.cleanseUrl(url), + data, + headers: this._buildHeaders(), + timeout: this._config.timeout, + withCredentials: this._config.withCredentials, + }) + } + + async delete(url: string, headers?: Record): Promise> { + return this._request({ + method: 'DELETE', + url: this.getBackendUrl() + this.cleanseUrl(url), + headers: this._buildHeaders(headers), + timeout: this._config.timeout, + withCredentials: this._config.withCredentials, + }) + } + + async patch(url: string, data?: any): Promise> { + return this._request({ + method: 'PATCH', + url: this.getBackendUrl() + this.cleanseUrl(url), + data, + headers: this._buildHeaders(), + timeout: this._config.timeout, + withCredentials: this._config.withCredentials, + }) + } + + /** + * Fetch a CSRF cookie. The default path matches Laravel Sanctum + * but you can pass any path your backend uses. + */ + async csrf(path = '/sanctum/csrf-cookie'): Promise> { + return this.get(path) + } + + // ------------------------------------------------------------------------- + // Error / success handlers + // ------------------------------------------------------------------------- + + /** + * Extract a readable error message, optionally show a notification, and re-throw. + * Designed as a `.catch()` handler: `api.get('x').catch(api.parseError)` + */ + parseError = (e: any): never => { + console.error(e) + + const text = + e?.response?.data?.message ?? + e?.response?.message ?? + e?.message ?? + e?.response?.data?.errors ?? + 'An unknown error occurred' + + if (this._notify && !this.silentErrors) { + this._notify({ + text: typeof text === 'string' ? text : JSON.stringify(text), + type: 'error', + errors: e?.response?.data?.errors, + timeout: 3000, + }) + } + + throw e + } + + /** + * Show a success notification from a response. + * Usage: `api.post('x').then(api.parseThen)` + */ + parseThen = (res: any, fallback?: string): void => { + const msg = res?.data?.message ?? res?.message ?? fallback ?? 'Success' + if (this._notify) { + this._notify(typeof msg === 'string' ? msg : JSON.stringify(msg)) + } + } +} + +// --------------------------------------------------------------------------- +// Factory +// --------------------------------------------------------------------------- + +export function createApiClient(config: ApiClientConfig): ApiClient { + return new ApiClient(config) +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..ebfbcec --- /dev/null +++ b/src/index.ts @@ -0,0 +1,21 @@ +// Core (framework-agnostic) +export { createApiClient, fetchAdapter } from './api' +export type { ApiClient } from './api' +export { createWsClient } from './ws' +export type { WsClient, WsChannel } from './ws' + +// Types & adapters +export type { + NotifyFn, + NotifyOptions, + TranslateFn, + StorageAdapter, + HttpAdapter, + HttpResponse, + HttpRequestConfig, + ApiClientConfig, + WsClientConfig, + ReactiveRef, + CreateRefFn, +} from './types' +export { browserStorage, memoryStorage, plainRef } from './types' diff --git a/src/nuxt.ts b/src/nuxt.ts new file mode 100644 index 0000000..30954ba --- /dev/null +++ b/src/nuxt.ts @@ -0,0 +1,103 @@ +import { ref } from 'vue' +import { createApiClient, type ApiClient } from './api' +import { createWsClient, type WsClient } from './ws' +import type { ApiClientConfig, WsClientConfig, ReactiveRef } from './types' + +/** + * Options for `createFromNuxtConfig()`. + * + * Override the runtimeConfig key names if your project uses different names. + * All keys read from `useRuntimeConfig().public`. + */ +export interface NuxtNetworkingOptions { + /** runtimeConfig key for REST API base URL (default: `'SERVER_URL'`) */ + serverUrlKey?: string + /** runtimeConfig key for internal SSR URL (default: `'SERVER_URL_INTERNAL'`) */ + serverUrlInternalKey?: string + /** runtimeConfig key for WebSocket URL (default: `'WEBS_URL'`) */ + wsUrlKey?: string + /** runtimeConfig key for WS protocol — `'wss'` or `'ws'` (default: `'WS_PROTOCOL'`) */ + wsProtocolKey?: string + + /** Additional ApiClientConfig overrides */ + apiConfig?: Partial + /** Additional WsClientConfig overrides */ + wsConfig?: Partial +} + +/** + * Create api + ws clients pre-wired for a Nuxt 3 app. + * + * Reads URLs from `useRuntimeConfig().public`, detects SSR via `import.meta.server`, + * and uses Vue `ref()` for WS reactive state. + * + * ```ts + * // plugins/networking.client.ts + * import { createFromNuxtConfig } from '@blax-software/networking/nuxt' + * + * export default defineNuxtPlugin(() => { + * const { api, ws } = createFromNuxtConfig() + * return { provide: { api, ws } } + * }) + * ``` + */ +export function createFromNuxtConfig(options: NuxtNetworkingOptions = {}): { + api: ApiClient + ws: WsClient +} { + // Nuxt auto-imports — these are available at plugin/composable scope + // @ts-expect-error Nuxt auto-import + const config = useRuntimeConfig() + + const serverUrlKey = options.serverUrlKey ?? 'SERVER_URL' + const serverUrlInternalKey = options.serverUrlInternalKey ?? 'SERVER_URL_INTERNAL' + const wsUrlKey = options.wsUrlKey ?? 'WEBS_URL' + const wsProtocolKey = options.wsProtocolKey ?? 'WS_PROTOCOL' + + const pub = config.public ?? config + const serverUrl: string = pub[serverUrlKey] ?? '' + const serverUrlInternal: string = pub[serverUrlInternalKey] ?? '' + const wsUrl: string = pub[wsUrlKey] ?? '' + const wsProtocol: string = pub[wsProtocolKey] ?? 'wss' + + // @ts-expect-error Nuxt/Vite global + const isServer: boolean = import.meta.server ?? false + + // --- API Client --- + const api = createApiClient({ + serverUrl, + ssrServerUrl: serverUrlInternal || undefined, + isServer: () => isServer, + defaultHeaders: () => { + if (!isServer) return {} + try { + // @ts-expect-error Nuxt auto-import + return useRequestHeaders(['cookie', 'x-forwarded-for', 'x-real-ip']) ?? {} + } catch { + return {} + } + }, + ...options.apiConfig, + }) + + // --- WS Client --- + // Nuxt always has Vue — use ref directly for reactive state + const vueRef = (initial: T): ReactiveRef => ref(initial) as ReactiveRef + + const ws = createWsClient( + { + url: `${wsProtocol === 'wss' ? 'wss' : 'ws'}://${wsUrl}/app/ws`, + isServer: () => isServer, + ...options.wsConfig, + }, + vueRef, + ) + + return { api, ws } +} + +// Re-export for convenience +export { createApiClient } from './api' +export { createWsClient } from './ws' +export type { ApiClient } from './api' +export type { WsClient, WsChannel } from './ws' diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9445774 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,199 @@ +// --------------------------------------------------------------------------- +// Notification adapter +// --------------------------------------------------------------------------- + +export interface NotifyOptions { + id?: string | number + text: string + type: 'success' | 'error' | 'warning' | 'info' + timeout?: number + errors?: any +} + +export type NotifyFn = (opts: NotifyOptions | string, type?: NotifyOptions['type']) => void +export type TranslateFn = (key: string) => string | null + +// --------------------------------------------------------------------------- +// Storage adapter (replaces direct localStorage usage) +// --------------------------------------------------------------------------- + +export interface StorageAdapter { + get(key: string): string | null + set(key: string, value: string): void + remove(key: string): void +} + +/** Default browser localStorage adapter. Safe for SSR (returns null on failure). */ +export const browserStorage: StorageAdapter = { + get: (key) => { + try { return localStorage.getItem(key) } catch { return null } + }, + set: (key, value) => { + try { localStorage.setItem(key, value) } catch {} + }, + remove: (key) => { + try { localStorage.removeItem(key) } catch {} + }, +} + +/** In-memory storage. Use for SSR or test environments. */ +export const memoryStorage = (): StorageAdapter => { + const store = new Map() + return { + get: (key) => store.get(key) ?? null, + set: (key, value) => { store.set(key, value) }, + remove: (key) => { store.delete(key) }, + } +} + +// --------------------------------------------------------------------------- +// HTTP adapter interface (fetch or axios or anything else) +// --------------------------------------------------------------------------- + +export interface HttpResponse { + data: T + status: number + headers: Record +} + +export interface HttpAdapter { + request(config: HttpRequestConfig): Promise> +} + +export interface HttpRequestConfig { + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + url: string + data?: any + headers?: Record + params?: Record + timeout?: number + withCredentials?: boolean +} + +// --------------------------------------------------------------------------- +// API client config +// --------------------------------------------------------------------------- + +export interface ApiClientConfig { + /** Base URL for HTTP requests. String or getter for dynamic resolution. */ + serverUrl: string | (() => string) + + /** + * Optional separate URL for server-side requests (e.g. internal Docker network). + * Only used when `isServer` returns true. + */ + ssrServerUrl?: string | (() => string) + + /** HTTP adapter. Defaults to built-in fetch adapter. */ + http?: HttpAdapter + + /** Notification callback for errors and success messages. */ + notify?: NotifyFn + + /** Token persistence adapter. Defaults to browserStorage. */ + storage?: StorageAdapter + + /** Key under which the bearer token is stored. Default: `'bearerToken'` */ + storageKey?: string + + /** Default request timeout in ms. Default: `10000` */ + timeout?: number + + /** Whether to include credentials (cookies). Default: `true` */ + withCredentials?: boolean + + /** Auto-retry on HTTP 503. Default: `true` */ + retryOn503?: boolean + + /** Max retry attempts on 503. Default: `2` */ + maxRetries?: number + + /** + * Prefix prepended to relative URL paths. + * E.g. `'api/'` turns `'users'` into `'api/users'`. + * Default: `'api/'` + */ + apiPrefix?: string + + /** Extra default headers merged into every request. */ + defaultHeaders?: Record | (() => Record) + + /** + * Whether we are running on the server (SSR). + * Affects URL resolution and storage. + * Default: `false` + */ + isServer?: boolean | (() => boolean) +} + +// --------------------------------------------------------------------------- +// WebSocket client config +// --------------------------------------------------------------------------- + +export interface WsClientConfig { + /** Full WebSocket URL (e.g. `'wss://example.com/app/ws'`). String or getter. */ + url: string | (() => string) + + /** Called on each channel establishment to get the current auth token. */ + getAuthToken?: () => string | null | undefined + + /** Notification callback for connection state changes. */ + notify?: NotifyFn + + /** Translation function for connection state messages. */ + translate?: TranslateFn + + /** Default channel name. Default: `'websocket'` */ + defaultChannel?: string + + /** Heartbeat (ping) interval in ms. Default: `20000` */ + heartbeatInterval?: number + + /** Delay in ms before attempting reconnection. Default: `3000` */ + reconnectDelay?: number + + /** Minimum ms between reconnect attempts. Default: `3000` */ + reconnectThrottle?: number + + /** Auto-reconnect on connection loss. Default: `true` */ + autoReconnect?: boolean + + /** + * Show connection lost/restored notifications. + * Set to `false` on native mobile apps where a toast overlay may be unwanted. + * Default: `true` + */ + showConnectionNotifications?: boolean + + /** + * Native platform check (e.g. Capacitor). + * When true, suppresses connection-state notifications. + */ + isNativePlatform?: boolean | (() => boolean) + + /** Whether running on the server (SSR). Returns an inert stub when true. */ + isServer?: boolean | (() => boolean) + + /** Fired when connection state changes. */ + onConnectionStateChange?: (state: 'connecting' | 'connected' | 'disconnected' | 'reconnecting') => void +} + +// --------------------------------------------------------------------------- +// Reactive ref abstraction (works with Vue, React state, or plain objects) +// --------------------------------------------------------------------------- + +/** Minimal reactive value container. Compatible with Vue `ref()` or a plain object. */ +export interface ReactiveRef { + value: T +} + +/** + * Factory function for creating reactive refs. + * - Vue users pass `ref` from `'vue'` + * - React users can wrap `useState` + * - Others use `plainRef` (default) + */ +export type CreateRefFn = (initial: T) => ReactiveRef + +/** Non-reactive ref — plain object with a `.value` property. */ +export const plainRef = (initial: T): ReactiveRef => ({ value: initial }) diff --git a/src/vue.ts b/src/vue.ts new file mode 100644 index 0000000..2ba576f --- /dev/null +++ b/src/vue.ts @@ -0,0 +1,85 @@ +import { ref, onUnmounted } from 'vue' +import { createApiClient, type ApiClient } from './api' +import { createWsClient, type WsClient } from './ws' +import type { + ApiClientConfig, + WsClientConfig, + CreateRefFn, + ReactiveRef, +} from './types' + +// --------------------------------------------------------------------------- +// Vue ref adapter +// --------------------------------------------------------------------------- + +const vueRef: CreateRefFn = (initial: T): ReactiveRef => ref(initial) as ReactiveRef + +// --------------------------------------------------------------------------- +// Composables +// --------------------------------------------------------------------------- + +/** + * Create (or return) an ApiClient. + * Convenience wrapper — you can also call `createApiClient()` directly. + */ +export function useApiClient(config: ApiClientConfig): ApiClient { + return createApiClient(config) +} + +/** + * Create a WsClient with Vue `ref()` for reactive state. + * `ws.is_setup`, `ws.is_opened`, etc. are Vue refs. + */ +export function useWsClient(config: WsClientConfig): WsClient { + return createWsClient(config, vueRef) +} + +/** + * Listen for a WS event with automatic cleanup on component unmount. + * + * ```ts + * useWsListener(ws, 'chat.message', null, (data) => { ... }) + * ``` + */ +export function useWsListener( + ws: WsClient, + event: string, + channel: string | null | undefined, + callback: (data: any) => void, +): () => void { + const off = ws.listen(event, channel, callback) + onUnmounted(off) + return off +} + +/** + * Resolve once when a WS event fires. Cleans up automatically if the component unmounts first. + */ +export function useWsListenOnce( + ws: WsClient, + event: string, + channel?: string | null, +): Promise { + let off: (() => void) | null = null + + const promise = new Promise((resolve) => { + off = ws.listen(event, channel, (data) => { + off?.() + off = null + resolve(data) + }) + }) + + onUnmounted(() => { + off?.() + }) + + return promise +} + +// Re-export everything from core for convenience +export { createApiClient } from './api' +export { createWsClient } from './ws' +export { vueRef } +export type { ApiClient } from './api' +export type { WsClient, WsChannel } from './ws' diff --git a/src/ws.ts b/src/ws.ts new file mode 100644 index 0000000..9a00705 --- /dev/null +++ b/src/ws.ts @@ -0,0 +1,629 @@ +import type { + WsClientConfig, + NotifyFn, + TranslateFn, + ReactiveRef, + CreateRefFn, +} from './types' +import { plainRef } from './types' + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface WsSocket extends WebSocket { + socket_id?: string +} + +export interface WsChannel { + name: string + is_established: boolean + establish(): Promise + send(event: string, data?: any): Promise + unsubscribe(): Promise +} + +export interface WsClient { + socket: WsSocket | null + channels: WsChannel[] + + is_opened: ReactiveRef + is_setup: ReactiveRef + is_connecting_socket: ReactiveRef + is_after_lost_connection: ReactiveRef + + heartbeat: ReturnType | null + last_reconnect_try: number + send_queue: any[] + + connect(force_reset?: boolean): Promise + ensureConnected(): Promise + channel(channel_name?: string | null): Promise + send( + event: string, + data?: object, + channel_name?: string | null, + progress?: (data: any) => void, + ): Promise + unsubscribe(channel_name?: string | null): Promise + + /** + * Listen for a WS event. Returns an unsubscribe function. + * Works with any framework's cleanup (React useEffect, Vue onUnmounted, etc.) + */ + listen(event: string, channel_name: string | null | undefined, callback: (data: any) => void): () => void + + /** Resolve once when the given event fires. */ + listenOnce(event: string, channel_name?: string | null): Promise + + /** Signal that app initialization is complete. Unblocks gated send() calls. */ + setAppReady(): void + + /** Force channels to re-subscribe with the current auth token on next send(). */ + resetConnection(): void + + /** Update configuration at runtime. */ + configure(partial: Partial): void + + /** Close the connection, clear intervals, and reset all state. */ + destroy(): void +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const _isProtocolEvent = (event: string): boolean => + /[.:](?:subscribe|unsubscribe|ping|pong)$/.test(event) + +// --------------------------------------------------------------------------- +// SSR stub — safe no-op returned when isServer is true +// --------------------------------------------------------------------------- + +function createSsrStub(createRef: CreateRefFn): WsClient { + return { + socket: null, + channels: [], + is_opened: createRef(false), + is_setup: createRef(false), + is_connecting_socket: createRef(false), + is_after_lost_connection: createRef(false), + heartbeat: null, + last_reconnect_try: 0, + send_queue: [], + connect: () => Promise.resolve(), + ensureConnected: () => Promise.resolve(), + channel: () => Promise.resolve(undefined), + send: () => Promise.resolve(null as any), + unsubscribe: () => Promise.resolve(true), + listen: () => () => {}, + listenOnce: () => Promise.resolve(null), + setAppReady: () => {}, + resetConnection: () => {}, + configure: () => {}, + destroy: () => {}, + } +} + +// --------------------------------------------------------------------------- +// Channel +// --------------------------------------------------------------------------- + +class WebsocketChannel implements WsChannel { + name: string + is_established = false + _establishPromise: Promise | null = null + private _ws: WsClientImpl + + constructor(name: string, ws: WsClientImpl) { + this.name = name + this._ws = ws + ws.channels.push(this) + } + + async establish(): Promise { + if (this.is_established && this._ws.is_setup.value) return this + this._establishPromise ??= this._doEstablish() + return this._establishPromise + } + + private async _doEstablish(): Promise { + try { + await this._ws.ensureConnected() + const authtoken = this._ws._getAuthToken() + await this._ws.send( + 'websocket.subscribe', + { channel: this.name, authtoken: authtoken ?? undefined }, + null, + ) + this.is_established = true + this._ws.is_setup.value = true + return this + } catch (error) { + this.is_established = false + this._establishPromise = null + throw error + } + } + + async send(event: string, data: any = {}): Promise { + if (!_isProtocolEvent(event) && !this.is_established) await this.establish() + return this._ws.send(event, data, _isProtocolEvent(event) ? null : this.name) + } + + async unsubscribe(): Promise { + if (this.is_established) { + await this._ws + .send('websocket.unsubscribe', { channel: this.name }, this.name) + .catch(() => {}) + } + this._ws.channels = this._ws.channels.filter((c) => c !== this) + return true + } +} + +// --------------------------------------------------------------------------- +// Main WS client +// --------------------------------------------------------------------------- + +class WsClientImpl extends EventTarget implements WsClient { + private _config: WsClientConfig + private _notify: NotifyFn | undefined + private _translate: TranslateFn | undefined + + socket: WsSocket | null = null + channels: WsChannel[] = [] + + is_opened: ReactiveRef + is_setup: ReactiveRef + is_connecting_socket: ReactiveRef + is_after_lost_connection: ReactiveRef + + heartbeat: ReturnType | null = null + last_reconnect_try = 0 + send_queue: any[] = [] + + // App-readiness gate + _appReady = false + private _appReadyResolve: (() => void) | null = null + private _appReadyPromise: Promise + + // Connection-ready promise + private _connectedResolve: (() => void) | null = null + private _connectedPromise: Promise | null = null + + // Connect promise coalescing + private _connectPromise: Promise | null = null + + constructor(config: WsClientConfig, createRef: CreateRefFn) { + super() + this._config = { + defaultChannel: 'websocket', + heartbeatInterval: 20_000, + reconnectDelay: 3000, + reconnectThrottle: 3000, + autoReconnect: true, + showConnectionNotifications: true, + ...config, + } + this._notify = config.notify + this._translate = config.translate + + this.is_opened = createRef(false) + this.is_setup = createRef(false) + this.is_connecting_socket = createRef(false) + this.is_after_lost_connection = createRef(false) + + this._appReadyPromise = new Promise((r) => { + this._appReadyResolve = r + }) + } + + // ------------------------------------------------------------------------- + // Configuration + // ------------------------------------------------------------------------- + + configure(partial: Partial): void { + Object.assign(this._config, partial) + if (partial.notify !== undefined) this._notify = partial.notify + if (partial.translate !== undefined) this._translate = partial.translate + } + + /** @internal — called by channels to obtain the current auth token */ + _getAuthToken(): string | null | undefined { + return this._config.getAuthToken?.() + } + + private _isNativePlatform(): boolean { + const v = this._config.isNativePlatform + return typeof v === 'function' ? v() : (v ?? false) + } + + private _getUrl(): string { + const u = this._config.url + return typeof u === 'function' ? u() : u + } + + private _t(key: string, fallback: string): string { + if (this._translate) { + const result = this._translate(key) + if (result) return result + } + return fallback + } + + private _shouldNotify(): boolean { + return ( + this._config.showConnectionNotifications !== false && + !this._isNativePlatform() && + !!this._notify + ) + } + + // ------------------------------------------------------------------------- + // Connection lifecycle + // ------------------------------------------------------------------------- + + ensureConnected(): Promise { + if (this.socket?.socket_id && this.is_opened.value) return Promise.resolve() + if (!this._connectedPromise) { + this._connectedPromise = new Promise((r) => { + this._connectedResolve = r + }) + this.connect().catch(() => {}) + } + return this._connectedPromise + } + + setAppReady(): void { + this._appReady = true + this._appReadyResolve?.() + } + + resetConnection(): void { + for (const ch of this.channels) { + const c = ch as WebsocketChannel + c.is_established = false + c._establishPromise = null + } + this.is_setup.value = false + } + + async connect(force_reset = false): Promise { + if (force_reset && this.socket) { + try { + this.socket.close() + } catch {} + this.socket = null + this._connectPromise = null + } + + if (this.socket && this.socket.readyState !== WebSocket.CLOSED) return this.socket + if (this._connectPromise) return this._connectPromise + + const throttle = this._config.reconnectThrottle ?? 3000 + if (this.last_reconnect_try && Date.now() - this.last_reconnect_try < throttle) { + console.log('[ws] Reconnect too fast, skipping') + return + } + + this.last_reconnect_try = Date.now() + this._connectPromise = this._doConnect(force_reset) + return this._connectPromise + } + + private _doConnect(force_reset: boolean): Promise { + const hbInterval = this._config.heartbeatInterval ?? 20_000 + + if (force_reset || !this.heartbeat) { + if (this.heartbeat) clearInterval(this.heartbeat) + this.heartbeat = setInterval(() => { + if (this.socket?.readyState === WebSocket.OPEN) { + this.socket.send('{"event":"websocket.ping","data":{}}') + } + }, hbInterval) + } + + if (force_reset) this.channels = [] + + const url = this._getUrl() + this.socket = new WebSocket(url) as WsSocket + this.is_connecting_socket.value = true + this._config.onConnectionStateChange?.('connecting') + + return new Promise((resolve, reject) => { + const socket = this.socket! + + socket.addEventListener('error', () => { + this.is_connecting_socket.value = false + this._connectPromise = null + reject(new Error('WebSocket error')) + }) + + socket.addEventListener('close', () => { + this.channels = [] + this.is_connecting_socket.value = false + this.is_opened.value = false + this.is_setup.value = false + this.socket = null + this._connectPromise = null + this._connectedPromise = null + this._connectedResolve = null + this.is_after_lost_connection.value = true + + if (this._shouldNotify()) { + const text = this._t('websocket.connectionlost', 'Connection lost. Reconnecting…') + this._notify!({ id: 'websocket-connection-state', type: 'info', text, timeout: 50_000 }) + } + + this._config.onConnectionStateChange?.('disconnected') + + if (this._appReady && this._config.autoReconnect !== false) { + const delay = this._config.reconnectDelay ?? 3000 + this._config.onConnectionStateChange?.('reconnecting') + setTimeout(() => this.connect().catch(() => {}), delay) + } + + reject(new Error('Socket closed')) + }) + + socket.addEventListener('open', () => { + if (this.is_after_lost_connection.value && this._shouldNotify()) { + const text = this._t('websocket.connectionrestored', 'Connection restored') + this._notify!({ id: 'websocket-connection-state', type: 'success', text, timeout: 1000 }) + } + + this.is_opened.value = true + this._config.onConnectionStateChange?.('connected') + // Warmup ping + socket.send('{"event":"websocket.ping","data":{}}') + }) + + socket.addEventListener('message', (raw) => { + const msg = JSON.parse(raw.data) + + if (msg?.event === 'websocket.connection_established') { + const data = JSON.parse(msg.data) + if (data?.socket_id && this.socket) { + this.socket.socket_id = data.socket_id + this.is_connecting_socket.value = false + resolve(this.socket) + this._connectedResolve?.() + // Proactively establish the default channel + this.channel() + this._workSendQueue() + } + return + } + + // Parse stringified data payloads + if (msg?.data && typeof msg.data === 'string') { + try { + msg.data = JSON.parse(msg.data) + } catch {} + } + + this.dispatchEvent( + new CustomEvent(msg.event, { + detail: { event: msg.event, data: msg.data, channel: msg.channel }, + }), + ) + }) + }) + } + + // ------------------------------------------------------------------------- + // Channel management + // ------------------------------------------------------------------------- + + async channel(channel_name: string | null = null): Promise { + channel_name ??= this._config.defaultChannel ?? 'websocket' + const existing = this.channels.find((c) => c.name === channel_name) + return (existing ?? new WebsocketChannel(channel_name, this)).establish() + } + + private async _workSendQueue(): Promise { + if (!this.send_queue.length) return + const queue = this.send_queue + this.send_queue = [] + for (const payload of queue) { + await this.channel(payload.channel) + this.socket?.send(JSON.stringify(payload)) + } + } + + // ------------------------------------------------------------------------- + // Send / receive + // ------------------------------------------------------------------------- + + async send( + event: string, + data: object = {}, + channel_name: string | null = null, + progress?: (data: any) => void, + _retryOnSubscriptionLost = true, + ): Promise { + // Gate non-protocol events until app signals readiness + if (!this._appReady && !_isProtocolEvent(event)) { + await this._appReadyPromise + } + + channel_name ??= this._config.defaultChannel ?? 'websocket' + if (!this.socket) await this.connect() + + // Build unique event suffix so the server response can be matched to this call + let sendingevent: string + if (event === 'websocket.subscribe') { + sendingevent = 'websocket.subscribe' + channel_name = null + } else { + sendingevent = event + '[' + Math.random().toString(36).substring(7) + ']' + } + + const payload = { event: sendingevent, data, channel: channel_name } + + // Ensure the target channel is subscribed + if (channel_name && !_isProtocolEvent(event)) { + await this.channel(channel_name) + } + + // Send or queue + if (this.is_opened.value && this.socket?.readyState === WebSocket.OPEN) { + this.socket.send(JSON.stringify(payload)) + } else { + this.send_queue.push(payload) + } + + await this.connect() + if (!this.socket) throw new Error('Socket not connected') + + const startTime = + typeof performance !== 'undefined' ? performance.now() : Date.now() + + return new Promise((resolve, reject) => { + const cleanup = () => { + this.removeEventListener(sendingevent + ':progress', handler) + this.removeEventListener(sendingevent + ':error', handler) + this.removeEventListener(sendingevent + ':response', handler) + } + + const handler = (m: any) => { + const msg = m.detail + const duration = Math.round( + (typeof performance !== 'undefined' ? performance.now() : Date.now()) - + startTime, + ) + + // Success + if ( + (event === 'websocket.subscribe' && + msg?.data?.channel === channel_name) || + msg?.event?.includes(sendingevent + ':response') + ) { + cleanup() + console.log(`[ws] ${sendingevent} ${duration}ms`) + resolve(msg.data) + return + } + + // Error / timeout + if ( + msg?.event?.includes(sendingevent + ':error') || + msg?.event?.includes(sendingevent + ':timeout') + ) { + cleanup() + console.log(`[ws] ${sendingevent} failed ${duration}ms`) + reject(msg.data) + return + } + + // Progress + if (progress && msg?.event?.includes(sendingevent + ':progress')) { + progress(msg.data) + } + } + + this.addEventListener(sendingevent + ':progress', handler) + this.addEventListener(sendingevent + ':error', handler) + this.addEventListener(sendingevent + ':response', handler) + }).catch((error: any) => { + // If the server dropped our subscription, re-establish and retry once + if ( + _retryOnSubscriptionLost && + error?.message === 'Subscription not established' + ) { + const ch = this.channels.find( + (c) => c.name === channel_name, + ) as WebsocketChannel | undefined + if (ch) { + ch.is_established = false + ch._establishPromise = null + } + console.log('[ws] Re-establishing channel after subscription loss') + return this.send(event, data, channel_name, progress, false) + } + throw error + }) as Promise + } + + // ------------------------------------------------------------------------- + // Event listeners (framework-agnostic — return cleanup function) + // ------------------------------------------------------------------------- + + listen( + event: string, + channel_name: string | null | undefined, + callback: (data: any) => void, + ): () => void { + channel_name ??= this._config.defaultChannel ?? 'websocket' + const handler = (m: any) => { + if (m.detail.channel === channel_name) callback(m.detail.data) + } + this.addEventListener(event, handler) + return () => this.removeEventListener(event, handler) + } + + listenOnce(event: string, channel_name?: string | null): Promise { + channel_name ??= this._config.defaultChannel ?? 'websocket' + return new Promise((resolve) => { + const handler = (m: any) => { + if (m.detail.channel === channel_name) { + resolve(m.detail.data) + this.removeEventListener(event, handler) + } + } + this.addEventListener(event, handler) + }) + } + + async unsubscribe(channel_name?: string | null): Promise { + channel_name ??= this._config.defaultChannel ?? 'websocket' + const channel = this.channels.find((c) => c.name === channel_name) + if (channel) await channel.unsubscribe() + this.channels = this.channels.filter((c) => c.name !== channel_name) + return true + } + + // ------------------------------------------------------------------------- + // Lifecycle + // ------------------------------------------------------------------------- + + destroy(): void { + if (this.heartbeat) clearInterval(this.heartbeat) + this.heartbeat = null + if (this.socket) { + try { + this.socket.close() + } catch {} + this.socket = null + } + this.channels = [] + this.send_queue = [] + this.is_opened.value = false + this.is_setup.value = false + this.is_connecting_socket.value = false + } +} + +// --------------------------------------------------------------------------- +// Factory +// --------------------------------------------------------------------------- + +/** + * Create a WebSocket client. + * + * @param config - Connection and behavior configuration. + * @param createRef - Reactive ref factory. Pass `ref` from Vue for reactive state, + * or omit for plain `{ value }` objects. + */ +export function createWsClient( + config: WsClientConfig, + createRef: CreateRefFn = plainRef, +): WsClient { + const isServer = + typeof config.isServer === 'function' + ? config.isServer() + : (config.isServer ?? false) + if (isServer) return createSsrStub(createRef) + return new WsClientImpl(config, createRef) +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4ebb529 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..c7399ef --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: { + index: 'src/index.ts', + vue: 'src/vue.ts', + nuxt: 'src/nuxt.ts', + 'api-axios': 'src/api-axios.ts', + }, + format: ['esm', 'cjs'], + dts: true, + splitting: true, + clean: true, + treeshake: true, + external: ['vue', 'axios'], +})