@nozich/api 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -2,6 +2,10 @@ import { ApiResponse } from '@nozich/types';
2
2
  import { nanoid } from 'nanoid';
3
3
  import SuperJSON from 'superjson';
4
4
 
5
+ export * from './src/contracts/auth';
6
+ export * from './src/contracts/chat';
7
+ export * from './src/contracts/fortune';
8
+
5
9
  export interface ApiConfig {
6
10
  baseUrl: string;
7
11
  projectId: string;
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@nozich/api",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "scripts": {
7
- "build": "tsc",
7
+ "clean": "rm -rf dist",
8
+ "build": "npm run clean && tsc",
9
+ "prepublishOnly": "npm run build",
10
+ "publish:npm": "npm publish --access public",
8
11
  "validate:api": "bun run validate-standards.ts",
9
12
  "validate:ws": "bun run validate-ws.ts"
10
13
  },
@@ -14,7 +17,7 @@
14
17
  "dependencies": {
15
18
  "@nozich/types": "*",
16
19
  "nanoid": "^5.1.6",
17
- "superjson": "^2.2.1"
20
+ "superjson": "^2.2.6"
18
21
  },
19
22
  "publishConfig": {
20
23
  "access": "public",
@@ -22,7 +25,7 @@
22
25
  },
23
26
  "repository": {
24
27
  "type": "git",
25
- "url": "git+https://github.com/anilcan-kara/nozich-api.git",
28
+ "url": "git+https://github.com/anilcan-kara/api.git",
26
29
  "directory": "packages/api"
27
30
  }
28
31
  }
@@ -0,0 +1,25 @@
1
+ import { User } from '@nozich/types';
2
+
3
+ export interface AuthContract {
4
+ 'auth.login': {
5
+ payload: {
6
+ email: string;
7
+ password?: string;
8
+ provider?: string;
9
+ token?: string;
10
+ };
11
+ response: { token: string; user: User };
12
+ };
13
+ 'auth.register': {
14
+ payload: { email: string; password?: string; name?: string };
15
+ response: { token: string; user: User };
16
+ };
17
+ 'auth.me': {
18
+ payload: {};
19
+ response: { user: User };
20
+ };
21
+ 'auth.refresh': {
22
+ payload: { refreshToken: string };
23
+ response: { token: string };
24
+ };
25
+ }
@@ -0,0 +1,23 @@
1
+ export interface Message {
2
+ role: 'system' | 'user' | 'assistant';
3
+ content: string;
4
+ }
5
+
6
+ export interface ChatContract {
7
+ 'chat.completions': {
8
+ payload: {
9
+ messages: Message[];
10
+ model?: string;
11
+ stream?: boolean;
12
+ };
13
+ // For stream=false
14
+ response: {
15
+ content: string;
16
+ usage?: { prompt: number; completion: number };
17
+ };
18
+ };
19
+ 'chat.history': {
20
+ payload: { userId: string; limit?: number };
21
+ response: { messages: Message[] };
22
+ };
23
+ }
@@ -0,0 +1,34 @@
1
+ export enum FortuneType {
2
+ COFFEE = 'coffee',
3
+ TAROT = 'tarot',
4
+ ASTROLOGY = 'astrology',
5
+ }
6
+
7
+ export interface FortuneTeller {
8
+ id: string;
9
+ name: string;
10
+ rating: number;
11
+ specialties: string[];
12
+ isOnline: boolean;
13
+ }
14
+
15
+ export interface FortuneContract {
16
+ 'fortune.tellers.list': {
17
+ payload: { type?: FortuneType; onlineOnly?: boolean };
18
+ response: { tellers: FortuneTeller[] };
19
+ };
20
+ 'fortune.consultations.create': {
21
+ payload: {
22
+ tellerId: string;
23
+ type: FortuneType;
24
+ question?: string;
25
+ media?: string[]; // URLs
26
+ birthDate?: string;
27
+ };
28
+ response: { consultationId: string; status: 'pending' | 'accepted' };
29
+ };
30
+ 'fortune.consultations.list': {
31
+ payload: { status?: string };
32
+ response: { consultations: any[] };
33
+ };
34
+ }
@@ -1,22 +0,0 @@
1
- export declare enum CircuitState {
2
- CLOSED = 0,
3
- OPEN = 1,
4
- HALF_OPEN = 2
5
- }
6
- interface CircuitBreakerOptions {
7
- failureThreshold: number;
8
- resetTimeoutMs: number;
9
- }
10
- export declare class CircuitBreaker {
11
- private state;
12
- private failures;
13
- private lastFailureTime;
14
- private readonly failureThreshold;
15
- private readonly resetTimeoutMs;
16
- constructor(options?: CircuitBreakerOptions);
17
- execute<T>(action: () => Promise<T>): Promise<T>;
18
- private onSuccess;
19
- private onFailure;
20
- getState(): CircuitState;
21
- }
22
- export {};
@@ -1,52 +0,0 @@
1
- export var CircuitState;
2
- (function (CircuitState) {
3
- CircuitState[CircuitState["CLOSED"] = 0] = "CLOSED";
4
- CircuitState[CircuitState["OPEN"] = 1] = "OPEN";
5
- CircuitState[CircuitState["HALF_OPEN"] = 2] = "HALF_OPEN";
6
- })(CircuitState || (CircuitState = {}));
7
- export class CircuitBreaker {
8
- constructor(options = {
9
- failureThreshold: 5,
10
- resetTimeoutMs: 10000,
11
- }) {
12
- this.state = CircuitState.CLOSED;
13
- this.failures = 0;
14
- this.lastFailureTime = 0;
15
- this.failureThreshold = options.failureThreshold;
16
- this.resetTimeoutMs = options.resetTimeoutMs;
17
- }
18
- async execute(action) {
19
- if (this.state === CircuitState.OPEN) {
20
- if (Date.now() - this.lastFailureTime > this.resetTimeoutMs) {
21
- this.state = CircuitState.HALF_OPEN;
22
- }
23
- else {
24
- throw new Error('Circuit Breaker is OPEN');
25
- }
26
- }
27
- try {
28
- const result = await action();
29
- this.onSuccess();
30
- return result;
31
- }
32
- catch (error) {
33
- this.onFailure();
34
- throw error;
35
- }
36
- }
37
- onSuccess() {
38
- this.failures = 0;
39
- this.state = CircuitState.CLOSED;
40
- }
41
- onFailure() {
42
- this.failures++;
43
- this.lastFailureTime = Date.now();
44
- if (this.state === CircuitState.HALF_OPEN ||
45
- this.failures >= this.failureThreshold) {
46
- this.state = CircuitState.OPEN;
47
- }
48
- }
49
- getState() {
50
- return this.state;
51
- }
52
- }
package/dist/index.d.ts DELETED
@@ -1,44 +0,0 @@
1
- import { ApiResponse } from '@nozich/types';
2
- export interface ApiConfig {
3
- baseUrl: string;
4
- projectId: string;
5
- tokens?: {
6
- getToken?: () => string | null | Promise<string | null>;
7
- };
8
- adapters?: {
9
- socket?: any;
10
- notifications?: any;
11
- };
12
- }
13
- export declare class ApiClient {
14
- private readonly config;
15
- private ws;
16
- private wsFailures;
17
- private forceHttp;
18
- private readonly MAX_WS_FAILURES;
19
- private readonly circuitBreaker;
20
- private wsConnected;
21
- private readonly listeners;
22
- private readonly pendingRequests;
23
- private heartbeatInterval;
24
- constructor(config: ApiConfig);
25
- private getToken;
26
- setToken(token: string): Promise<void>;
27
- subscribe<T>(event: string, callback: (payload: T) => void): () => void;
28
- unsubscribe(event: string, callback: (payload: any) => void): void;
29
- private connectWs;
30
- private startHeartbeat;
31
- private sendWs;
32
- private dispatch;
33
- private handleWsError;
34
- request<T>(endpoint: string, options?: RequestInit & {
35
- silent?: boolean;
36
- }): Promise<ApiResponse<T>>;
37
- private requestHttp;
38
- get<T>(endpoint: string, query?: any, options?: RequestInit): Promise<ApiResponse<T>>;
39
- post<T>(endpoint: string, body?: any, options?: RequestInit): Promise<ApiResponse<T>>;
40
- put<T>(endpoint: string, body?: any, options?: RequestInit): Promise<ApiResponse<T>>;
41
- patch<T>(endpoint: string, body?: any, options?: RequestInit): Promise<ApiResponse<T>>;
42
- delete<T>(endpoint: string, body?: any, options?: RequestInit): Promise<ApiResponse<T>>;
43
- }
44
- export declare const createApiClient: (config: ApiConfig) => ApiClient;
package/dist/index.js DELETED
@@ -1,284 +0,0 @@
1
- import { nanoid } from 'nanoid';
2
- import SuperJSON from 'superjson';
3
- import { CircuitBreaker } from './circuit-breaker';
4
- export class ApiClient {
5
- constructor(config) {
6
- this.ws = null;
7
- this.wsFailures = 0;
8
- this.forceHttp = false;
9
- this.MAX_WS_FAILURES = 3;
10
- this.wsConnected = false;
11
- this.listeners = new Map();
12
- this.pendingRequests = new Map();
13
- this.heartbeatInterval = null;
14
- this.config = config;
15
- this.circuitBreaker = new CircuitBreaker();
16
- this.connectWs();
17
- }
18
- async getToken() {
19
- if (this.config.tokens?.getToken) {
20
- return await this.config.tokens.getToken();
21
- }
22
- return null;
23
- }
24
- async setToken(token) {
25
- // Legacy support? Or just rely on getToken?
26
- // In new design, we pull token from callback.
27
- // But socket might need immediate push.
28
- if (this.wsConnected && this.ws) {
29
- this.sendWs({ type: 'auth.identify', payload: { token }, id: nanoid() });
30
- }
31
- }
32
- subscribe(event, callback) {
33
- if (!this.listeners.has(event)) {
34
- this.listeners.set(event, []);
35
- }
36
- this.listeners.get(event)?.push(callback);
37
- return () => this.unsubscribe(event, callback);
38
- }
39
- unsubscribe(event, callback) {
40
- const list = this.listeners.get(event);
41
- if (!list)
42
- return;
43
- this.listeners.set(event, list.filter((cb) => cb !== callback));
44
- }
45
- connectWs() {
46
- if (this.config.adapters?.socket) {
47
- // Use external socket adapter if provided (e.g. from Panel's useSocket store)
48
- this.ws = this.config.adapters.socket;
49
- return;
50
- }
51
- if (this.forceHttp)
52
- return;
53
- if (typeof WebSocket === 'undefined')
54
- return;
55
- try {
56
- const wsUrl = this.config.baseUrl.replace('http', 'ws') + '/ws';
57
- this.ws = new WebSocket(wsUrl);
58
- this.ws.onopen = async () => {
59
- this.wsFailures = 0;
60
- this.wsConnected = true;
61
- console.log('[API] WS Connected');
62
- this.startHeartbeat();
63
- const token = await this.getToken();
64
- // Auto-identify if token exists
65
- if (token) {
66
- this.sendWs({
67
- type: 'auth.identify',
68
- payload: { token },
69
- id: nanoid(),
70
- });
71
- }
72
- };
73
- this.ws.onmessage = (event) => {
74
- try {
75
- const data = JSON.parse(event.data.toString());
76
- // 1. Handle Request Correlation (Standard)
77
- if (data.id && this.pendingRequests.has(data.id)) {
78
- const req = this.pendingRequests.get(data.id);
79
- if (req) {
80
- clearTimeout(req.timer);
81
- this.pendingRequests.delete(data.id);
82
- if (data.type === 'error') {
83
- req.reject(new Error(data.payload?.message || 'Unknown WS Error'));
84
- }
85
- else {
86
- req.resolve(data.payload);
87
- }
88
- return; // Handled as response
89
- }
90
- }
91
- // 2. Handle Events
92
- if (data.type) {
93
- this.dispatch(data.type, data.payload);
94
- }
95
- }
96
- catch (e) {
97
- console.error('[API] WS Parse Error', e);
98
- }
99
- };
100
- this.ws.onerror = () => {
101
- this.handleWsError();
102
- };
103
- this.ws.onclose = () => {
104
- this.wsConnected = false;
105
- if (this.heartbeatInterval)
106
- clearInterval(this.heartbeatInterval);
107
- // Reject all pending requests
108
- for (const [id, req] of this.pendingRequests) {
109
- clearTimeout(req.timer);
110
- req.reject(new Error('WS Closed'));
111
- }
112
- this.pendingRequests.clear();
113
- // Exponential backoff
114
- if (!this.forceHttp) {
115
- const delay = Math.min(30000, 1000 * Math.pow(2, this.wsFailures));
116
- console.log(`[API] Reconnecting WS in ${delay}ms...`);
117
- setTimeout(() => this.connectWs(), delay);
118
- }
119
- };
120
- }
121
- catch (e) {
122
- this.handleWsError();
123
- }
124
- }
125
- startHeartbeat() {
126
- if (this.heartbeatInterval)
127
- clearInterval(this.heartbeatInterval);
128
- this.heartbeatInterval = setInterval(() => {
129
- if (this.ws?.readyState === WebSocket.OPEN) {
130
- this.ws.send(JSON.stringify({ type: 'ping', payload: Date.now() }));
131
- }
132
- }, 30000);
133
- }
134
- sendWs(event) {
135
- if (this.ws?.readyState === WebSocket.OPEN) {
136
- this.ws.send(JSON.stringify(event));
137
- }
138
- }
139
- dispatch(type, payload) {
140
- const callbacks = this.listeners.get(type);
141
- callbacks?.forEach((cb) => cb(payload));
142
- }
143
- handleWsError() {
144
- this.wsFailures++;
145
- console.warn(`[API] WS Error (${this.wsFailures}/${this.MAX_WS_FAILURES})`);
146
- if (this.wsFailures >= this.MAX_WS_FAILURES) {
147
- this.forceHttp = true;
148
- console.error('[API] WS Circuit Breaker Tripped. Locked to HTTP.');
149
- this.ws?.close();
150
- this.ws = null;
151
- }
152
- }
153
- async request(endpoint, options = {}) {
154
- const isSock = this.ws &&
155
- this.ws.readyState === WebSocket.OPEN &&
156
- !this.forceHttp &&
157
- options.method !== 'HEAD'; // HEAD not supported over WS usually?
158
- // In original code: const isSock = useSocket.getState().readyState === socketStates.OPEN && opt.type !== 'fetch';
159
- // We try to use WS if connected.
160
- if (isSock) {
161
- return new Promise((resolve, reject) => {
162
- const id = nanoid();
163
- const timer = setTimeout(() => {
164
- if (this.pendingRequests.has(id)) {
165
- this.pendingRequests.delete(id);
166
- console.warn(`[API] WS Request Timeout for ${endpoint}, falling back to HTTP`);
167
- this.requestHttp(endpoint, options).then(resolve).catch(reject);
168
- }
169
- }, 5000); // 5s timeout
170
- this.pendingRequests.set(id, { resolve, reject, timer });
171
- this.sendWs({
172
- type: 'api.request',
173
- payload: {
174
- path: endpoint,
175
- method: options?.method || 'GET',
176
- body: options?.body
177
- ? JSON.parse(options.body)
178
- : undefined,
179
- },
180
- id,
181
- });
182
- });
183
- }
184
- // 2. HTTP Fallback with Circuit Breaker
185
- return this.requestHttp(endpoint, options);
186
- }
187
- async requestHttp(endpoint, options = {}) {
188
- try {
189
- return await this.circuitBreaker.execute(async () => {
190
- let url = `${this.config.baseUrl}/${endpoint}`;
191
- // Handle GET query params if body is present (SuperJSON behavior from original file)
192
- if (['GET', 'HEAD'].includes(options.method || 'GET') && options.body) {
193
- // It seems options.body is stringified already by wrapper methods?
194
- // Wrapper methods below create JSON string.
195
- // We need to parse it back if we want to put it in QP? Or just trust caller?
196
- // Original: path = path + '?' + new URLSearchParams({ q: SuperJSON.stringify(opt.body) }).toString();
197
- // Here options.body is currently stringified JSON standard.
198
- // Let's assume wrapper passes object if it's GET? No, wrapper stringifies.
199
- // We should change wrapper to NOT stringify for GET?
200
- }
201
- const token = await this.getToken();
202
- const headers = {
203
- 'Content-Type': 'application/json',
204
- 'X-Project-ID': this.config.projectId,
205
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
206
- ...options?.headers,
207
- };
208
- const res = await fetch(url, { ...options, headers });
209
- const text = await res.text();
210
- // SuperJSON parse? Or standard?
211
- // Original used:
212
- // if (isJson(message)) { message = JSON.parse(message); }
213
- // Let's standard JSON parse for now unless SuperJSON is enforced by server.
214
- // If server returns SuperJSON, we need SuperJSON.parse.
215
- let json;
216
- try {
217
- json = JSON.parse(text);
218
- }
219
- catch {
220
- return text;
221
- }
222
- if (!res.ok) {
223
- const error = json.error || {
224
- code: `HTTP_${res.status}`,
225
- message: json.message || res.statusText,
226
- details: json.details,
227
- };
228
- throw new Error(error.message || JSON.stringify(error));
229
- }
230
- return json;
231
- });
232
- }
233
- catch (error) {
234
- if (!options.silent && this.config.adapters?.notifications) {
235
- this.config.adapters.notifications.show({
236
- color: 'red',
237
- title: 'Error',
238
- message: error.message || 'Unknown Error',
239
- });
240
- }
241
- throw error;
242
- }
243
- }
244
- get(endpoint, query, options) {
245
- // If query is object, we should maybe append to URL?
246
- // Original code: if (['GET', 'HEAD'].includes(opt.method) && opt.body) ... q=SuperJSON(body)
247
- if (query) {
248
- endpoint =
249
- endpoint +
250
- '?' +
251
- new URLSearchParams({ q: SuperJSON.stringify(query) }).toString();
252
- }
253
- return this.request(endpoint, { ...options, method: 'GET' });
254
- }
255
- post(endpoint, body, options) {
256
- return this.request(endpoint, {
257
- ...options,
258
- method: 'POST',
259
- body: SuperJSON.stringify(body),
260
- });
261
- }
262
- put(endpoint, body, options) {
263
- return this.request(endpoint, {
264
- ...options,
265
- method: 'PUT',
266
- body: SuperJSON.stringify(body),
267
- });
268
- }
269
- patch(endpoint, body, options) {
270
- return this.request(endpoint, {
271
- ...options,
272
- method: 'PATCH',
273
- body: SuperJSON.stringify(body),
274
- });
275
- }
276
- delete(endpoint, body, options) {
277
- return this.request(endpoint, {
278
- ...options,
279
- method: 'DELETE',
280
- body: body ? SuperJSON.stringify(body) : undefined,
281
- });
282
- }
283
- }
284
- export const createApiClient = (config) => new ApiClient(config);