@neevjs/client 0.0.1 → 1.0.0-beta

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/dist/index.js CHANGED
@@ -25,27 +25,41 @@ __export(index_exports, {
25
25
  CachePlugin: () => CachePlugin,
26
26
  Form: () => Form,
27
27
  LoggerPlugin: () => LoggerPlugin,
28
+ NeevBoundary: () => NeevBoundary,
28
29
  NeevContext: () => NeevContext,
29
30
  NeevProvider: () => NeevProvider,
30
31
  OfflinePlugin: () => OfflinePlugin,
31
32
  Protected: () => Protected,
33
+ SecureStore: () => SecureStore,
32
34
  Table: () => Table,
35
+ VERSION: () => import_shared2.VERSION,
36
+ clearPersistedStore: () => clearPersistedStore,
33
37
  createCachePlugin: () => createCachePlugin,
34
38
  createClient: () => createClient,
35
39
  createOfflinePlugin: () => createOfflinePlugin,
40
+ getStore: () => getStore,
41
+ setStore: () => setStore,
36
42
  useAuth: () => useAuth,
37
43
  useModel: () => useModel,
38
44
  useNeevClient: () => useNeevClient,
45
+ useStore: () => useStore,
39
46
  useSyncStatus: () => useSyncStatus
40
47
  });
41
48
  module.exports = __toCommonJS(index_exports);
49
+ var import_shared2 = require("@neevjs/shared");
42
50
 
43
51
  // src/core/AuthClient.ts
52
+ var USER_CACHE_KEY = "neev_user";
44
53
  var AuthClient = class {
45
54
  client;
46
55
  userData = null;
47
56
  constructor(client) {
48
57
  this.client = client;
58
+ try {
59
+ const cached = sessionStorage.getItem(USER_CACHE_KEY);
60
+ if (cached) this.userData = JSON.parse(cached);
61
+ } catch {
62
+ }
49
63
  }
50
64
  async login(email, password) {
51
65
  const res = await this.client.request("/auth/login", {
@@ -54,6 +68,7 @@ var AuthClient = class {
54
68
  });
55
69
  localStorage.setItem("neev_token", res.data.token);
56
70
  this.userData = res.data.user;
71
+ sessionStorage.setItem(USER_CACHE_KEY, JSON.stringify(this.userData));
57
72
  }
58
73
  async register(email, password, name) {
59
74
  const res = await this.client.request("/auth/register", {
@@ -62,9 +77,11 @@ var AuthClient = class {
62
77
  });
63
78
  localStorage.setItem("neev_token", res.data.token);
64
79
  this.userData = res.data.user;
80
+ sessionStorage.setItem(USER_CACHE_KEY, JSON.stringify(this.userData));
65
81
  }
66
82
  logout() {
67
83
  localStorage.removeItem("neev_token");
84
+ sessionStorage.removeItem(USER_CACHE_KEY);
68
85
  this.userData = null;
69
86
  }
70
87
  async user() {
@@ -72,6 +89,7 @@ var AuthClient = class {
72
89
  if (this.userData) return this.userData;
73
90
  const res = await this.client.request("/auth/me");
74
91
  this.userData = res.data;
92
+ sessionStorage.setItem(USER_CACHE_KEY, JSON.stringify(this.userData));
75
93
  return this.userData;
76
94
  }
77
95
  isAuthenticated() {
@@ -93,13 +111,23 @@ function createClient(config = {}) {
93
111
  req = await plugin.onRequest(req);
94
112
  }
95
113
  }
114
+ if (req._cachedResponse instanceof Response) {
115
+ let cachedRes = req._cachedResponse;
116
+ for (const plugin of plugins) {
117
+ if (plugin.onResponse) {
118
+ cachedRes = await plugin.onResponse(cachedRes, req);
119
+ }
120
+ }
121
+ return cachedRes.json();
122
+ }
96
123
  const headers = {
97
124
  "Content-Type": "application/json",
98
125
  ...req.options.headers ?? {}
99
126
  };
100
127
  let response;
101
128
  try {
102
- response = await fetch(baseURL + req.url, {
129
+ const finalBaseURL = req.options.baseURL ?? baseURL;
130
+ response = await fetch(finalBaseURL + req.url, {
103
131
  method: req.options.method ?? "GET",
104
132
  body: req.options.body,
105
133
  headers
@@ -107,20 +135,31 @@ function createClient(config = {}) {
107
135
  let finalResponse = response;
108
136
  for (const plugin of plugins) {
109
137
  if (plugin.onResponse) {
110
- finalResponse = await plugin.onResponse(finalResponse);
138
+ finalResponse = await plugin.onResponse(finalResponse, req);
111
139
  }
112
140
  }
141
+ if (finalResponse.status === 401) {
142
+ client.auth?.logout();
143
+ const error = new Error("Session expired. Please log in again.");
144
+ error.status = 401;
145
+ for (const plugin of plugins) {
146
+ plugin.onError?.(error, req);
147
+ }
148
+ throw error;
149
+ }
113
150
  if (!finalResponse.ok) {
114
151
  const errorBody = await finalResponse.json().catch(() => ({ error: "Unknown error" }));
115
- throw new Error(
152
+ const error = new Error(
116
153
  typeof errorBody.error === "string" ? errorBody.error : `HTTP ${finalResponse.status}`
117
154
  );
155
+ error.status = finalResponse.status;
156
+ throw error;
118
157
  }
119
158
  return finalResponse.json();
120
159
  } catch (err) {
121
160
  const error = err instanceof Error ? err : new Error(String(err));
122
161
  for (const plugin of plugins) {
123
- plugin.onError?.(error);
162
+ plugin.onError?.(error, req);
124
163
  }
125
164
  throw error;
126
165
  }
@@ -133,7 +172,6 @@ function createClient(config = {}) {
133
172
  request,
134
173
  use,
135
174
  auth: null
136
- // set below after client is defined
137
175
  };
138
176
  client.auth = new AuthClient(client);
139
177
  return client;
@@ -156,15 +194,150 @@ function useNeevClient() {
156
194
  return client;
157
195
  }
158
196
 
197
+ // src/core/SecureStore.ts
198
+ var SECURE_PREFIX = "neev_secure_";
199
+ var _encryptionKey = null;
200
+ async function getEncryptionKey() {
201
+ if (_encryptionKey) return _encryptionKey;
202
+ if (!window.crypto?.subtle) {
203
+ throw new Error(
204
+ "[NeevJS] SecureStore requires Web Crypto API (available in HTTPS or localhost contexts only)."
205
+ );
206
+ }
207
+ _encryptionKey = await window.crypto.subtle.generateKey(
208
+ { name: "AES-GCM", length: 256 },
209
+ false,
210
+ // non-extractable — the raw key bytes can NEVER be exported
211
+ ["encrypt", "decrypt"]
212
+ );
213
+ return _encryptionKey;
214
+ }
215
+ async function encrypt(data) {
216
+ const key = await getEncryptionKey();
217
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
218
+ const encoded = new TextEncoder().encode(JSON.stringify(data));
219
+ const ciphertext = await window.crypto.subtle.encrypt(
220
+ { name: "AES-GCM", iv },
221
+ key,
222
+ encoded
223
+ );
224
+ const combined = new Uint8Array(iv.byteLength + ciphertext.byteLength);
225
+ combined.set(iv, 0);
226
+ combined.set(new Uint8Array(ciphertext), iv.byteLength);
227
+ return btoa(String.fromCharCode(...combined));
228
+ }
229
+ async function decrypt(encoded) {
230
+ const key = await getEncryptionKey();
231
+ const combined = Uint8Array.from(atob(encoded), (c) => c.charCodeAt(0));
232
+ const iv = combined.slice(0, 12);
233
+ const ciphertext = combined.slice(12);
234
+ const decrypted = await window.crypto.subtle.decrypt(
235
+ { name: "AES-GCM", iv },
236
+ key,
237
+ ciphertext
238
+ );
239
+ return JSON.parse(new TextDecoder().decode(decrypted));
240
+ }
241
+ var SecureStore = {
242
+ /**
243
+ * Encrypt and store a value in localStorage.
244
+ * The value cannot be read without the in-memory session key.
245
+ */
246
+ async set(key, value) {
247
+ const ciphertext = await encrypt(value);
248
+ localStorage.setItem(SECURE_PREFIX + key, ciphertext);
249
+ },
250
+ /**
251
+ * Retrieve and decrypt a value.
252
+ * Returns null if the key doesn't exist or cannot be decrypted
253
+ * (e.g. after a page refresh when the key is gone).
254
+ */
255
+ async get(key) {
256
+ const stored = localStorage.getItem(SECURE_PREFIX + key);
257
+ if (!stored) return null;
258
+ try {
259
+ return await decrypt(stored);
260
+ } catch {
261
+ localStorage.removeItem(SECURE_PREFIX + key);
262
+ return null;
263
+ }
264
+ },
265
+ /**
266
+ * Remove a specific key from SecureStore.
267
+ */
268
+ remove(key) {
269
+ localStorage.removeItem(SECURE_PREFIX + key);
270
+ },
271
+ /**
272
+ * Remove ALL SecureStore entries from localStorage.
273
+ * Call this on logout to clean up all session-bound encrypted data.
274
+ */
275
+ clear() {
276
+ const keysToRemove = [];
277
+ for (let i = 0; i < localStorage.length; i++) {
278
+ const k = localStorage.key(i);
279
+ if (k?.startsWith(SECURE_PREFIX)) keysToRemove.push(k);
280
+ }
281
+ keysToRemove.forEach((k) => localStorage.removeItem(k));
282
+ }
283
+ };
284
+
159
285
  // src/hooks/useModel.ts
160
286
  var import_react2 = require("react");
161
287
  var cache = /* @__PURE__ */ new Map();
162
- function useModel(name) {
288
+ var listeners = /* @__PURE__ */ new Map();
289
+ function emitChange(url, exclude) {
290
+ listeners.get(url)?.forEach((listener) => {
291
+ if (listener !== exclude) listener();
292
+ });
293
+ }
294
+ function mutateModel(url) {
295
+ cache.delete(url);
296
+ emitChange(url);
297
+ }
298
+ var inFlightRequests = /* @__PURE__ */ new Map();
299
+ var cacheTimestamps = /* @__PURE__ */ new Map();
300
+ var cacheError = /* @__PURE__ */ new Map();
301
+ var STALE_TIME = 60 * 1e3;
302
+ function useModel(name, options) {
163
303
  const client = useNeevClient();
164
- const url = `/${name}`;
304
+ const baseUrl = `/${name}`;
305
+ const url = options?.params ? `${baseUrl}?${new URLSearchParams(
306
+ Object.entries(options.params).filter(([, v]) => v !== void 0).map(([k, v]) => [k, String(v)])
307
+ ).toString()}` : baseUrl;
165
308
  const [data, setData] = (0, import_react2.useState)(() => cache.get(url) ?? []);
166
309
  const [loading, setLoading] = (0, import_react2.useState)(!cache.has(url));
167
310
  const [error, setError] = (0, import_react2.useState)(null);
311
+ if (options?.suspense) {
312
+ if (cacheError.has(url)) {
313
+ throw cacheError.get(url);
314
+ }
315
+ const timestamp = cacheTimestamps.get(url);
316
+ const isFresh = timestamp && Date.now() - timestamp < STALE_TIME;
317
+ if (!isFresh && !cache.has(url)) {
318
+ if (!navigator.onLine) {
319
+ throw new Error("[NeevJS] Offline: No cached data available for this model.");
320
+ }
321
+ let requestPromise = inFlightRequests.get(url);
322
+ if (!requestPromise) {
323
+ requestPromise = client.request(url, { baseURL: options?.baseURL }).then((res) => {
324
+ const rows = Array.isArray(res) ? res : res.data ?? [];
325
+ cache.set(url, rows);
326
+ cacheTimestamps.set(url, Date.now());
327
+ cacheError.delete(url);
328
+ }).catch((err) => {
329
+ if (!navigator.onLine && cache.has(url)) return;
330
+ cacheError.set(url, err instanceof Error ? err : new Error(String(err)));
331
+ }).finally(() => {
332
+ if (inFlightRequests.get(url) === requestPromise) {
333
+ inFlightRequests.delete(url);
334
+ }
335
+ });
336
+ inFlightRequests.set(url, requestPromise);
337
+ }
338
+ throw requestPromise;
339
+ }
340
+ }
168
341
  const mountedRef = (0, import_react2.useRef)(true);
169
342
  (0, import_react2.useEffect)(() => {
170
343
  mountedRef.current = true;
@@ -172,51 +345,138 @@ function useModel(name) {
172
345
  mountedRef.current = false;
173
346
  };
174
347
  }, []);
175
- const fetchData = (0, import_react2.useCallback)(async () => {
348
+ const fetchData = (0, import_react2.useCallback)(async (force = false) => {
349
+ if (!navigator.onLine && cache.has(url)) {
350
+ if (mountedRef.current) {
351
+ setData(cache.get(url));
352
+ setError(null);
353
+ setLoading(false);
354
+ }
355
+ return;
356
+ }
357
+ if (!force && cache.has(url)) {
358
+ const timestamp = cacheTimestamps.get(url);
359
+ if (timestamp && Date.now() - timestamp < STALE_TIME) {
360
+ if (mountedRef.current) {
361
+ setData(cache.get(url));
362
+ setError(null);
363
+ setLoading(false);
364
+ }
365
+ return;
366
+ }
367
+ }
176
368
  if (mountedRef.current) setLoading(true);
369
+ let requestPromise = inFlightRequests.get(url);
370
+ if (!requestPromise) {
371
+ requestPromise = client.request(url, { baseURL: options?.baseURL });
372
+ inFlightRequests.set(url, requestPromise);
373
+ }
177
374
  try {
178
- const res = await client.request(url);
375
+ const res = await requestPromise;
179
376
  const rows = Array.isArray(res) ? res : res.data ?? [];
180
377
  cache.set(url, rows);
378
+ cacheTimestamps.set(url, Date.now());
379
+ cacheError.delete(url);
181
380
  if (mountedRef.current) {
182
381
  setData(rows);
183
382
  setError(null);
184
383
  }
185
384
  } catch (err) {
186
- if (mountedRef.current) {
385
+ if (!navigator.onLine && cache.has(url)) {
386
+ if (mountedRef.current) {
387
+ setData(cache.get(url));
388
+ setError(null);
389
+ }
390
+ } else if (mountedRef.current) {
187
391
  setError(err instanceof Error ? err : new Error(String(err)));
188
392
  }
189
393
  } finally {
394
+ if (inFlightRequests.get(url) === requestPromise) {
395
+ inFlightRequests.delete(url);
396
+ }
190
397
  if (mountedRef.current) setLoading(false);
191
398
  }
192
399
  }, [client, url]);
193
400
  (0, import_react2.useEffect)(() => {
194
- void fetchData();
401
+ void fetchData(false);
402
+ }, [fetchData]);
403
+ const handleRemoteChange = (0, import_react2.useCallback)(() => {
404
+ void fetchData(true);
195
405
  }, [fetchData]);
406
+ (0, import_react2.useEffect)(() => {
407
+ if (!listeners.has(url)) listeners.set(url, /* @__PURE__ */ new Set());
408
+ listeners.get(url).add(handleRemoteChange);
409
+ return () => {
410
+ listeners.get(url)?.delete(handleRemoteChange);
411
+ };
412
+ }, [url, handleRemoteChange]);
196
413
  const create = (0, import_react2.useCallback)(async (payload) => {
197
- await client.request(url, {
198
- method: "POST",
199
- body: JSON.stringify(payload)
200
- });
201
- cache.delete(url);
202
- await fetchData();
203
- }, [client, url, fetchData]);
414
+ try {
415
+ await client.request(url, {
416
+ method: "POST",
417
+ body: JSON.stringify(payload),
418
+ baseURL: options?.baseURL
419
+ });
420
+ cache.delete(url);
421
+ await fetchData(true);
422
+ emitChange(url, handleRemoteChange);
423
+ } catch (err) {
424
+ if (err.message?.includes("[NeevJS] Offline")) {
425
+ const newRecord = { ...payload, id: `temp_${Date.now()}` };
426
+ const currentData = cache.get(url) ?? [];
427
+ const updatedData = [...currentData, newRecord];
428
+ cache.set(url, updatedData);
429
+ setData(updatedData);
430
+ emitChange(url, handleRemoteChange);
431
+ } else {
432
+ throw err;
433
+ }
434
+ }
435
+ }, [client, url, fetchData, handleRemoteChange]);
204
436
  const update = (0, import_react2.useCallback)(async (id, payload) => {
205
- await client.request(`${url}/${id}`, {
206
- method: "PUT",
207
- body: JSON.stringify(payload)
208
- });
209
- cache.delete(url);
210
- await fetchData();
211
- }, [client, url, fetchData]);
437
+ try {
438
+ await client.request(`${url}/${id}`, {
439
+ method: "PUT",
440
+ body: JSON.stringify(payload),
441
+ baseURL: options?.baseURL
442
+ });
443
+ cache.delete(url);
444
+ await fetchData(true);
445
+ emitChange(url, handleRemoteChange);
446
+ } catch (err) {
447
+ if (err.message?.includes("[NeevJS] Offline")) {
448
+ const currentData = cache.get(url) ?? [];
449
+ const updatedData = currentData.map((item) => String(item.id) === String(id) ? { ...item, ...payload } : item);
450
+ cache.set(url, updatedData);
451
+ setData(updatedData);
452
+ emitChange(url, handleRemoteChange);
453
+ } else {
454
+ throw err;
455
+ }
456
+ }
457
+ }, [client, url, fetchData, handleRemoteChange]);
212
458
  const remove = (0, import_react2.useCallback)(async (id) => {
213
- await client.request(`${url}/${id}`, {
214
- method: "DELETE"
215
- });
216
- cache.delete(url);
217
- await fetchData();
218
- }, [client, url, fetchData]);
219
- return { data, loading, error, create, update, remove, refresh: fetchData };
459
+ try {
460
+ await client.request(`${url}/${id}`, {
461
+ method: "DELETE",
462
+ baseURL: options?.baseURL
463
+ });
464
+ cache.delete(url);
465
+ await fetchData(true);
466
+ emitChange(url, handleRemoteChange);
467
+ } catch (err) {
468
+ if (err.message?.includes("[NeevJS] Offline")) {
469
+ const currentData = cache.get(url) ?? [];
470
+ const filteredData = currentData.filter((item) => String(item.id) !== String(id));
471
+ cache.set(url, filteredData);
472
+ setData(filteredData);
473
+ emitChange(url, handleRemoteChange);
474
+ } else {
475
+ throw err;
476
+ }
477
+ }
478
+ }, [client, url, fetchData, handleRemoteChange]);
479
+ return { data, loading, error, create, update, remove, refresh: () => fetchData(true) };
220
480
  }
221
481
 
222
482
  // src/hooks/useAuth.ts
@@ -236,6 +496,8 @@ function useAuth() {
236
496
  var import_react3 = require("react");
237
497
  var syncState = {
238
498
  pendingCount: 0,
499
+ syncing: false,
500
+ errors: [],
239
501
  listeners: /* @__PURE__ */ new Set(),
240
502
  notify() {
241
503
  this.listeners.forEach((fn) => fn());
@@ -244,18 +506,19 @@ var syncState = {
244
506
  function useSyncStatus() {
245
507
  const [isOffline, setIsOffline] = (0, import_react3.useState)(!navigator.onLine);
246
508
  const [pending, setPending] = (0, import_react3.useState)(syncState.pendingCount);
247
- const [syncing, setSyncing] = (0, import_react3.useState)(false);
509
+ const [syncing, setSyncing] = (0, import_react3.useState)(syncState.syncing);
510
+ const [errors, setErrors] = (0, import_react3.useState)(syncState.errors);
248
511
  (0, import_react3.useEffect)(() => {
249
512
  function onOnline() {
250
513
  setIsOffline(false);
251
- setSyncing(true);
252
- setTimeout(() => setSyncing(false), 2e3);
253
514
  }
254
515
  function onOffline() {
255
516
  setIsOffline(true);
256
517
  }
257
518
  function onSyncUpdate() {
258
519
  setPending(syncState.pendingCount);
520
+ setSyncing(syncState.syncing);
521
+ setErrors([...syncState.errors]);
259
522
  }
260
523
  window.addEventListener("online", onOnline);
261
524
  window.addEventListener("offline", onOffline);
@@ -266,7 +529,95 @@ function useSyncStatus() {
266
529
  syncState.listeners.delete(onSyncUpdate);
267
530
  };
268
531
  }, []);
269
- return { isOffline, pending, syncing };
532
+ function clearErrors() {
533
+ syncState.errors = [];
534
+ syncState.notify();
535
+ }
536
+ return { isOffline, pending, syncing, errors, clearErrors };
537
+ }
538
+
539
+ // src/hooks/useStore.ts
540
+ var import_react4 = require("react");
541
+ var stores = /* @__PURE__ */ new Map();
542
+ var listeners2 = /* @__PURE__ */ new Map();
543
+ function subscribe(key, callback) {
544
+ if (!listeners2.has(key)) listeners2.set(key, /* @__PURE__ */ new Set());
545
+ listeners2.get(key).add(callback);
546
+ return () => {
547
+ listeners2.get(key).delete(callback);
548
+ };
549
+ }
550
+ function emitChange2(key) {
551
+ listeners2.get(key)?.forEach((fn) => fn());
552
+ }
553
+ var PERSIST_PREFIX = "neev_store_";
554
+ var SESSION_PREFIX = "neev_session_";
555
+ function readFromStorage(key, backend, fallback) {
556
+ try {
557
+ const prefix = backend === "sessionStorage" ? SESSION_PREFIX : PERSIST_PREFIX;
558
+ const raw = window[backend].getItem(prefix + key);
559
+ if (raw === null) return fallback;
560
+ const entry = JSON.parse(raw);
561
+ if (entry.expiresAt !== void 0 && Date.now() > entry.expiresAt) {
562
+ window[backend].removeItem(prefix + key);
563
+ return fallback;
564
+ }
565
+ return entry.value;
566
+ } catch {
567
+ return fallback;
568
+ }
569
+ }
570
+ function writeToStorage(key, value, backend, ttl) {
571
+ try {
572
+ const prefix = backend === "sessionStorage" ? SESSION_PREFIX : PERSIST_PREFIX;
573
+ const entry = {
574
+ value,
575
+ expiresAt: ttl !== void 0 ? Date.now() + ttl : void 0
576
+ };
577
+ window[backend].setItem(prefix + key, JSON.stringify(entry));
578
+ } catch {
579
+ }
580
+ }
581
+ function useStore(key, initialValue, options = {}) {
582
+ const { persist = false, session = false, ttl } = options;
583
+ const backend = persist ? "localStorage" : session ? "sessionStorage" : null;
584
+ if (!stores.has(key)) {
585
+ const resolved = backend ? readFromStorage(key, backend, initialValue) : initialValue;
586
+ stores.set(key, resolved);
587
+ }
588
+ const snapshot = (0, import_react4.useSyncExternalStore)(
589
+ (0, import_react4.useCallback)((callback) => subscribe(key, callback), [key]),
590
+ (0, import_react4.useCallback)(() => stores.get(key), [key]),
591
+ (0, import_react4.useCallback)(() => initialValue, [])
592
+ // eslint-disable-line react-hooks/exhaustive-deps
593
+ );
594
+ const setValue = (0, import_react4.useCallback)(
595
+ (updater) => {
596
+ const current = stores.get(key);
597
+ const next = typeof updater === "function" ? updater(current) : updater;
598
+ stores.set(key, next);
599
+ if (backend) {
600
+ writeToStorage(key, next, backend, ttl);
601
+ }
602
+ emitChange2(key);
603
+ },
604
+ [key, backend, ttl]
605
+ );
606
+ return [snapshot, setValue];
607
+ }
608
+ function getStore(key) {
609
+ return stores.get(key);
610
+ }
611
+ function setStore(key, value) {
612
+ stores.set(key, value);
613
+ emitChange2(key);
614
+ }
615
+ function clearPersistedStore(key) {
616
+ try {
617
+ localStorage.removeItem(PERSIST_PREFIX + key);
618
+ sessionStorage.removeItem(SESSION_PREFIX + key);
619
+ } catch {
620
+ }
270
621
  }
271
622
 
272
623
  // src/components/Table.tsx
@@ -276,37 +627,44 @@ function Table({
276
627
  columns,
277
628
  onEdit,
278
629
  onDelete,
279
- emptyMessage = "No records found."
630
+ emptyMessage = "No records found.",
631
+ classNames = {},
632
+ styles = {},
633
+ unstyled = false
280
634
  }) {
281
635
  const { data, loading, error } = useModel(model);
282
636
  if (loading) {
283
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "neev-table-loading", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Loading..." }) });
637
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: classNames.loadingState ?? "neev-table-loading", style: styles.loadingState, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: "Loading..." }) });
284
638
  }
285
639
  if (error) {
286
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "neev-table-error", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
640
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: classNames.errorState ?? "neev-table-error", style: styles.errorState, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
287
641
  "Error: ",
288
642
  error.message
289
643
  ] }) });
290
644
  }
291
645
  if (data.length === 0) {
292
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "neev-table-empty", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: emptyMessage }) });
646
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: classNames.emptyState ?? "neev-table-empty", style: styles.emptyState, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: emptyMessage }) });
293
647
  }
294
648
  const resolvedColumns = columns ?? Object.keys(data[0]).map((key) => ({
295
649
  key,
296
650
  label: key.charAt(0).toUpperCase() + key.slice(1)
297
651
  }));
298
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "neev-table-wrapper", style: { overflowX: "auto" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { className: "neev-table", style: { width: "100%", borderCollapse: "collapse" }, children: [
299
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { children: [
652
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: classNames.root ?? "neev-table-wrapper", style: { ...!unstyled ? { overflowX: "auto" } : {}, ...styles.root }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("table", { className: classNames.table ?? "neev-table", style: { ...!unstyled ? { width: "100%", borderCollapse: "collapse" } : {}, ...styles.table }, children: [
653
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("thead", { className: classNames.thead, style: styles.thead, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("tr", { className: classNames.tr, style: styles.tr, children: [
300
654
  resolvedColumns.map((col) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
301
655
  "th",
302
656
  {
657
+ className: classNames.th,
303
658
  style: {
304
- textAlign: "left",
305
- padding: "10px 12px",
306
- borderBottom: "2px solid #e5e7eb",
307
- fontWeight: 600,
308
- color: "#374151",
309
- background: "#f9fafb"
659
+ ...!unstyled ? {
660
+ textAlign: "left",
661
+ padding: "10px 12px",
662
+ borderBottom: "2px solid #e5e7eb",
663
+ fontWeight: 600,
664
+ color: "#374151",
665
+ background: "#f9fafb"
666
+ } : {},
667
+ ...styles.th
310
668
  },
311
669
  children: col.label ?? col.key
312
670
  },
@@ -315,45 +673,55 @@ function Table({
315
673
  (onEdit || onDelete) && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
316
674
  "th",
317
675
  {
676
+ className: classNames.th,
318
677
  style: {
319
- textAlign: "left",
320
- padding: "10px 12px",
321
- borderBottom: "2px solid #e5e7eb",
322
- fontWeight: 600,
323
- color: "#374151",
324
- background: "#f9fafb"
678
+ ...!unstyled ? {
679
+ textAlign: "left",
680
+ padding: "10px 12px",
681
+ borderBottom: "2px solid #e5e7eb",
682
+ fontWeight: 600,
683
+ color: "#374151",
684
+ background: "#f9fafb"
685
+ } : {},
686
+ ...styles.th
325
687
  },
326
688
  children: "Actions"
327
689
  }
328
690
  )
329
691
  ] }) }),
330
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { children: data.map((row, rowIndex) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
692
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("tbody", { className: classNames.tbody, style: styles.tbody, children: data.map((row, rowIndex) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
331
693
  "tr",
332
694
  {
333
- style: { borderBottom: "1px solid #e5e7eb" },
695
+ className: classNames.tr,
696
+ style: { ...!unstyled ? { borderBottom: "1px solid #e5e7eb" } : {}, ...styles.tr },
334
697
  children: [
335
698
  resolvedColumns.map((col) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
336
699
  "td",
337
700
  {
338
- style: { padding: "10px 12px", color: "#1f2937", verticalAlign: "middle" },
701
+ className: classNames.td,
702
+ style: { ...!unstyled ? { padding: "10px 12px", color: "#1f2937", verticalAlign: "middle" } : {}, ...styles.td },
339
703
  children: col.render ? col.render(row[col.key], row) : String(row[col.key] ?? "")
340
704
  },
341
705
  col.key
342
706
  )),
343
- (onEdit || onDelete) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("td", { style: { padding: "10px 12px", verticalAlign: "middle" }, children: [
707
+ (onEdit || onDelete) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("td", { className: classNames.actions ?? classNames.td, style: { ...!unstyled ? { padding: "10px 12px", verticalAlign: "middle" } : {}, ...styles.td, ...styles.actions }, children: [
344
708
  onEdit && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
345
709
  "button",
346
710
  {
347
711
  onClick: () => onEdit(row),
712
+ className: classNames.editButton,
348
713
  style: {
349
- marginRight: 8,
350
- padding: "4px 10px",
351
- fontSize: 13,
352
- cursor: "pointer",
353
- background: "#3b82f6",
354
- color: "#fff",
355
- border: "none",
356
- borderRadius: 4
714
+ ...!unstyled ? {
715
+ marginRight: 8,
716
+ padding: "4px 10px",
717
+ fontSize: 13,
718
+ cursor: "pointer",
719
+ background: "#3b82f6",
720
+ color: "#fff",
721
+ border: "none",
722
+ borderRadius: 4
723
+ } : {},
724
+ ...styles.editButton
357
725
  },
358
726
  children: "Edit"
359
727
  }
@@ -362,14 +730,18 @@ function Table({
362
730
  "button",
363
731
  {
364
732
  onClick: () => onDelete(row),
733
+ className: classNames.deleteButton,
365
734
  style: {
366
- padding: "4px 10px",
367
- fontSize: 13,
368
- cursor: "pointer",
369
- background: "#ef4444",
370
- color: "#fff",
371
- border: "none",
372
- borderRadius: 4
735
+ ...!unstyled ? {
736
+ padding: "4px 10px",
737
+ fontSize: 13,
738
+ cursor: "pointer",
739
+ background: "#ef4444",
740
+ color: "#fff",
741
+ border: "none",
742
+ borderRadius: 4
743
+ } : {},
744
+ ...styles.deleteButton
373
745
  },
374
746
  children: "Delete"
375
747
  }
@@ -383,7 +755,7 @@ function Table({
383
755
  }
384
756
 
385
757
  // src/components/Form.tsx
386
- var import_react4 = require("react");
758
+ var import_react5 = require("react");
387
759
  var import_jsx_runtime3 = require("react/jsx-runtime");
388
760
  function Form({
389
761
  model,
@@ -393,22 +765,48 @@ function Form({
393
765
  onSuccess,
394
766
  onError,
395
767
  submitLabel = "Submit",
396
- children
768
+ classNames = {},
769
+ styles = {},
770
+ unstyled = false,
771
+ transformPayload,
772
+ onSubmitOverride,
773
+ children,
774
+ fieldErrors: externalFieldErrors = {},
775
+ validate
397
776
  }) {
398
777
  const { create, update } = useModel(model);
399
- const [submitting, setSubmitting] = (0, import_react4.useState)(false);
400
- const [formError, setFormError] = (0, import_react4.useState)(null);
778
+ const [submitting, setSubmitting] = (0, import_react5.useState)(false);
779
+ const [formError, setFormError] = (0, import_react5.useState)(null);
780
+ const [internalFieldErrors, setInternalFieldErrors] = (0, import_react5.useState)({});
781
+ const allFieldErrors = { ...internalFieldErrors, ...externalFieldErrors };
401
782
  async function handleSubmit(e) {
402
783
  e.preventDefault();
403
784
  setSubmitting(true);
404
785
  setFormError(null);
786
+ setInternalFieldErrors({});
405
787
  const formData = new FormData(e.currentTarget);
406
788
  const payload = Object.fromEntries(formData.entries());
407
789
  try {
408
- if (editId !== void 0) {
409
- await update(editId, payload);
790
+ let finalPayload = payload;
791
+ if (validate) {
792
+ const validationErrors = validate(finalPayload);
793
+ if (validationErrors && Object.keys(validationErrors).length > 0) {
794
+ setInternalFieldErrors(validationErrors);
795
+ setSubmitting(false);
796
+ return;
797
+ }
798
+ }
799
+ if (transformPayload) {
800
+ finalPayload = await transformPayload(finalPayload);
801
+ }
802
+ if (onSubmitOverride) {
803
+ await onSubmitOverride(finalPayload);
410
804
  } else {
411
- await create(payload);
805
+ if (editId !== void 0) {
806
+ await update(editId, finalPayload);
807
+ } else {
808
+ await create(finalPayload);
809
+ }
412
810
  }
413
811
  onSuccess?.();
414
812
  } catch (err) {
@@ -419,7 +817,7 @@ function Form({
419
817
  setSubmitting(false);
420
818
  }
421
819
  }
422
- const inputStyle = {
820
+ const baseInputStyle = unstyled ? {} : {
423
821
  display: "block",
424
822
  width: "100%",
425
823
  padding: "8px 10px",
@@ -430,29 +828,33 @@ function Form({
430
828
  boxSizing: "border-box",
431
829
  marginTop: 4
432
830
  };
433
- const labelStyle = {
831
+ const baseLabelStyle = unstyled ? {} : {
434
832
  display: "block",
435
833
  fontSize: 13,
436
834
  fontWeight: 500,
437
835
  color: "#374151",
438
836
  marginBottom: 12
439
837
  };
440
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { onSubmit: (e) => void handleSubmit(e), style: { width: "100%" }, children: [
838
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { className: classNames.root, onSubmit: (e) => void handleSubmit(e), style: { width: "100%", ...styles.root }, children: [
441
839
  formError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
442
840
  "div",
443
841
  {
842
+ className: classNames.error,
444
843
  style: {
445
- padding: "8px 12px",
446
- marginBottom: 12,
447
- background: "#fee2e2",
448
- color: "#dc2626",
449
- borderRadius: 6,
450
- fontSize: 13
844
+ ...!unstyled ? {
845
+ padding: "8px 12px",
846
+ marginBottom: 12,
847
+ background: "#fee2e2",
848
+ color: "#dc2626",
849
+ borderRadius: 6,
850
+ fontSize: 13
851
+ } : {},
852
+ ...styles.error
451
853
  },
452
854
  children: formError
453
855
  }
454
856
  ),
455
- fields?.map((field) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { style: labelStyle, children: [
857
+ fields?.map((field) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: classNames.label, style: { ...baseLabelStyle, ...styles.label }, children: [
456
858
  field.label ?? field.name,
457
859
  field.required && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: "#ef4444", marginLeft: 2 }, children: "*" }),
458
860
  field.type === "textarea" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -462,7 +864,8 @@ function Form({
462
864
  placeholder: field.placeholder,
463
865
  required: field.required,
464
866
  defaultValue: String(initialValues[field.name] ?? ""),
465
- style: { ...inputStyle, minHeight: 80, resize: "vertical" }
867
+ className: classNames.textarea ?? classNames.input,
868
+ style: { ...baseInputStyle, minHeight: 80, resize: "vertical", ...styles.input, ...styles.textarea }
466
869
  }
467
870
  ) : field.type === "select" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
468
871
  "select",
@@ -470,7 +873,8 @@ function Form({
470
873
  name: field.name,
471
874
  required: field.required,
472
875
  defaultValue: String(initialValues[field.name] ?? ""),
473
- style: inputStyle,
876
+ className: classNames.select ?? classNames.input,
877
+ style: { ...baseInputStyle, ...styles.input, ...styles.select },
474
878
  children: [
475
879
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: "", children: "Select..." }),
476
880
  field.options?.map((opt) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: opt.value, children: opt.label }, opt.value))
@@ -484,7 +888,24 @@ function Form({
484
888
  placeholder: field.placeholder,
485
889
  required: field.required,
486
890
  defaultValue: String(initialValues[field.name] ?? ""),
487
- style: inputStyle
891
+ className: classNames.input,
892
+ style: { ...baseInputStyle, ...styles.input }
893
+ }
894
+ ),
895
+ allFieldErrors[field.name] && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
896
+ "span",
897
+ {
898
+ className: classNames.fieldError,
899
+ style: {
900
+ ...!unstyled ? {
901
+ display: "block",
902
+ marginTop: 4,
903
+ fontSize: 12,
904
+ color: "#dc2626"
905
+ } : {},
906
+ ...styles.fieldError
907
+ },
908
+ children: allFieldErrors[field.name]
488
909
  }
489
910
  )
490
911
  ] }, field.name)),
@@ -494,17 +915,21 @@ function Form({
494
915
  {
495
916
  type: "submit",
496
917
  disabled: submitting,
918
+ className: classNames.submitButton,
497
919
  style: {
498
- marginTop: 8,
499
- padding: "9px 20px",
500
- background: submitting ? "#9ca3af" : "#111827",
501
- color: "#fff",
502
- border: "none",
503
- borderRadius: 6,
504
- fontSize: 14,
505
- fontWeight: 600,
506
- cursor: submitting ? "not-allowed" : "pointer",
507
- width: "100%"
920
+ ...!unstyled ? {
921
+ marginTop: 8,
922
+ padding: "9px 20px",
923
+ background: submitting ? "#9ca3af" : "#111827",
924
+ color: "#fff",
925
+ border: "none",
926
+ borderRadius: 6,
927
+ fontSize: 14,
928
+ fontWeight: 600,
929
+ cursor: submitting ? "not-allowed" : "pointer",
930
+ width: "100%"
931
+ } : {},
932
+ ...styles.submitButton
508
933
  },
509
934
  children: submitting ? "Saving..." : submitLabel
510
935
  }
@@ -513,36 +938,88 @@ function Form({
513
938
  }
514
939
 
515
940
  // src/components/Protected.tsx
516
- var import_react5 = require("react");
941
+ var import_react6 = require("react");
517
942
  var import_jsx_runtime4 = require("react/jsx-runtime");
518
- function Protected({ children, fallback, role }) {
943
+ function Protected({ children, loadingFallback = null, fallback, role }) {
519
944
  const { isAuthenticated, user } = useAuth();
520
- const [checking, setChecking] = (0, import_react5.useState)(true);
521
- const [allowed, setAllowed] = (0, import_react5.useState)(false);
522
- (0, import_react5.useEffect)(() => {
523
- async function check() {
524
- if (!isAuthenticated()) {
525
- setAllowed(false);
526
- setChecking(false);
527
- return;
528
- }
529
- if (role) {
530
- const u = await user();
945
+ const hasToken = isAuthenticated();
946
+ const [checking, setChecking] = (0, import_react6.useState)(hasToken && !!role);
947
+ const [allowed, setAllowed] = (0, import_react6.useState)(hasToken && !role);
948
+ (0, import_react6.useEffect)(() => {
949
+ if (!hasToken) {
950
+ setAllowed(false);
951
+ setChecking(false);
952
+ return;
953
+ }
954
+ if (!role) {
955
+ setAllowed(true);
956
+ setChecking(false);
957
+ return;
958
+ }
959
+ let cancelled = false;
960
+ async function checkRole() {
961
+ const u = await user();
962
+ if (!cancelled) {
531
963
  setAllowed(u?.role === role);
532
- } else {
533
- setAllowed(true);
964
+ setChecking(false);
534
965
  }
535
- setChecking(false);
536
966
  }
537
- void check();
538
- }, [isAuthenticated, role, user]);
539
- if (checking) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, {});
967
+ void checkRole();
968
+ return () => {
969
+ cancelled = true;
970
+ };
971
+ }, [hasToken, role, user]);
972
+ if (checking) {
973
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: loadingFallback });
974
+ }
540
975
  if (!allowed) {
541
976
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: fallback ?? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { padding: 24, textAlign: "center", color: "#6b7280" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { children: "You are not authorized to view this page." }) }) });
542
977
  }
543
978
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children });
544
979
  }
545
980
 
981
+ // src/components/NeevBoundary.tsx
982
+ var import_react7 = require("react");
983
+ var import_jsx_runtime5 = require("react/jsx-runtime");
984
+ var NeevErrorBoundary = class extends import_react7.Component {
985
+ constructor(props) {
986
+ super(props);
987
+ this.state = { hasError: false, error: null };
988
+ }
989
+ static getDerivedStateFromError(error) {
990
+ return { hasError: true, error };
991
+ }
992
+ componentDidCatch(error, errorInfo) {
993
+ console.error("NeevBoundary caught an error:", error, errorInfo);
994
+ }
995
+ resetErrorBoundary = () => {
996
+ this.setState({ hasError: false, error: null });
997
+ };
998
+ render() {
999
+ if (this.state.hasError && this.state.error) {
1000
+ if (this.props.fallback) {
1001
+ return this.props.fallback(this.state.error, this.resetErrorBoundary);
1002
+ }
1003
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: { padding: "1rem", background: "#fee2e2", color: "#b91c1c", borderRadius: "8px", fontFamily: "sans-serif" }, children: [
1004
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h3", { style: { margin: "0 0 8px 0" }, children: "Data Error" }),
1005
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: { margin: 0, fontSize: "14px" }, children: this.state.error.message }),
1006
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1007
+ "button",
1008
+ {
1009
+ onClick: this.resetErrorBoundary,
1010
+ style: { marginTop: "12px", padding: "6px 12px", background: "#b91c1c", color: "white", border: "none", borderRadius: "4px", cursor: "pointer", fontSize: "13px" },
1011
+ children: "Try again"
1012
+ }
1013
+ )
1014
+ ] });
1015
+ }
1016
+ return this.props.children;
1017
+ }
1018
+ };
1019
+ function NeevBoundary({ children, loadingFallback, errorFallback }) {
1020
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(NeevErrorBoundary, { fallback: errorFallback, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_react7.Suspense, { fallback: loadingFallback ?? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { padding: "1rem", color: "#6b7280", fontFamily: "sans-serif" }, children: "Loading data..." }), children }) });
1021
+ }
1022
+
546
1023
  // src/plugins/AuthPlugin.ts
547
1024
  var AuthPlugin = {
548
1025
  name: "auth",
@@ -562,18 +1039,19 @@ var AuthPlugin = {
562
1039
  };
563
1040
 
564
1041
  // src/plugins/LoggerPlugin.ts
1042
+ var import_shared = require("@neevjs/shared");
565
1043
  var LoggerPlugin = {
566
1044
  name: "logger",
567
1045
  onRequest(req) {
568
- console.log(`[NeevJS] \u2192 ${req.options.method ?? "GET"} ${req.url}`);
1046
+ console.log(`[NeevJS v${import_shared.VERSION}] \u2192 ${req.options.method ?? "GET"} ${req.url}`);
569
1047
  return req;
570
1048
  },
571
- onResponse(res) {
572
- console.log(`[NeevJS] \u2190 ${res.status} ${res.url}`);
1049
+ onResponse(res, req) {
1050
+ console.log(`[NeevJS v${import_shared.VERSION}] \u2190 ${res.status} ${req.options.method ?? "GET"} ${req.url}`);
573
1051
  return res;
574
1052
  },
575
- onError(err) {
576
- console.error("[NeevJS] \u2717 Error:", err.message);
1053
+ onError(err, req) {
1054
+ console.error(`[NeevJS v${import_shared.VERSION}] \u2717 Error on ${req?.url ?? "unknown"}:`, err.message);
577
1055
  }
578
1056
  };
579
1057
 
@@ -585,25 +1063,36 @@ function createCachePlugin(options = {}) {
585
1063
  name: "cache",
586
1064
  onRequest(req) {
587
1065
  const method = (req.options.method ?? "GET").toUpperCase();
588
- if (method !== "GET") return req;
1066
+ if (method !== "GET") {
1067
+ store.delete(req.url);
1068
+ const parts = req.url.split("/");
1069
+ if (parts.length > 2) {
1070
+ const baseUrl = "/" + parts[1];
1071
+ store.delete(baseUrl);
1072
+ }
1073
+ return req;
1074
+ }
589
1075
  const entry = store.get(req.url);
590
1076
  if (entry && Date.now() < entry.expiresAt) {
591
1077
  const cached = new Response(JSON.stringify(entry.data), {
592
1078
  status: 200,
593
- headers: { "Content-Type": "application/json", "X-Neev-Cache": "HIT" }
1079
+ headers: {
1080
+ "Content-Type": "application/json",
1081
+ "X-Neev-Cache": "HIT"
1082
+ }
594
1083
  });
595
1084
  return { ...req, _cachedResponse: cached };
596
1085
  }
597
1086
  return req;
598
1087
  },
599
- async onResponse(res) {
600
- if (res.headers.get("X-Neev-Cache") === "HIT") return res;
601
- const url = res.url;
602
- if (!url) return res;
1088
+ async onResponse(res, req) {
1089
+ if (res.headers.get("X-Neev-Cache") === "HIT" || !res.ok) return res;
1090
+ const method = (req.options.method ?? "GET").toUpperCase();
1091
+ if (method !== "GET") return res;
603
1092
  try {
604
1093
  const clone = res.clone();
605
1094
  const data = await clone.json();
606
- store.set(url, { data, expiresAt: Date.now() + ttl });
1095
+ store.set(req.url, { data, expiresAt: Date.now() + ttl });
607
1096
  } catch {
608
1097
  }
609
1098
  return res;
@@ -634,27 +1123,54 @@ function saveQueue(queue) {
634
1123
  }
635
1124
  function createOfflinePlugin() {
636
1125
  let client;
1126
+ let onlineHandler = null;
637
1127
  async function processQueue() {
638
1128
  if (!navigator.onLine) return;
639
1129
  const queue = loadQueue();
640
1130
  if (queue.length === 0) return;
1131
+ syncState.syncing = true;
1132
+ syncState.notify();
641
1133
  const remaining = [];
1134
+ let hasChanges = false;
1135
+ const urlsToMutate = /* @__PURE__ */ new Set();
642
1136
  for (const action of queue) {
643
1137
  try {
644
1138
  await client.request(action.url, action.options);
645
- } catch {
646
- remaining.push({ ...action, retries: action.retries + 1 });
1139
+ hasChanges = true;
1140
+ const baseUrl = "/" + (action.url.split("/")[1] || "");
1141
+ urlsToMutate.add(baseUrl);
1142
+ } catch (err) {
1143
+ if (err.status && err.status >= 400 && err.status < 500) {
1144
+ syncState.errors.push(new Error(`Offline action on ${action.url} failed: ${err.message}`));
1145
+ hasChanges = true;
1146
+ const baseUrl = "/" + (action.url.split("/")[1] || "");
1147
+ urlsToMutate.add(baseUrl);
1148
+ } else {
1149
+ remaining.push({ ...action, retries: action.retries + 1 });
1150
+ }
647
1151
  }
648
1152
  }
649
1153
  saveQueue(remaining);
1154
+ syncState.syncing = false;
1155
+ syncState.notify();
1156
+ if (hasChanges) {
1157
+ urlsToMutate.forEach((url) => mutateModel(url));
1158
+ }
650
1159
  }
651
- window.addEventListener("online", () => {
652
- void processQueue();
653
- });
654
1160
  return {
655
1161
  name: "offline",
656
1162
  setup(c) {
657
1163
  client = c;
1164
+ if (onlineHandler) {
1165
+ window.removeEventListener("online", onlineHandler);
1166
+ }
1167
+ onlineHandler = () => {
1168
+ void processQueue();
1169
+ };
1170
+ window.addEventListener("online", onlineHandler);
1171
+ if (navigator.onLine && loadQueue().length > 0) {
1172
+ setTimeout(() => void processQueue(), 500);
1173
+ }
658
1174
  },
659
1175
  onRequest(req) {
660
1176
  const method = (req.options.method ?? "GET").toUpperCase();
@@ -684,16 +1200,23 @@ var OfflinePlugin = createOfflinePlugin();
684
1200
  CachePlugin,
685
1201
  Form,
686
1202
  LoggerPlugin,
1203
+ NeevBoundary,
687
1204
  NeevContext,
688
1205
  NeevProvider,
689
1206
  OfflinePlugin,
690
1207
  Protected,
1208
+ SecureStore,
691
1209
  Table,
1210
+ VERSION,
1211
+ clearPersistedStore,
692
1212
  createCachePlugin,
693
1213
  createClient,
694
1214
  createOfflinePlugin,
1215
+ getStore,
1216
+ setStore,
695
1217
  useAuth,
696
1218
  useModel,
697
1219
  useNeevClient,
1220
+ useStore,
698
1221
  useSyncStatus
699
1222
  });