@anfenn/dync 1.0.13 → 1.0.15

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.
@@ -1,4 +1,4 @@
1
- import { a as ApiFunctions, h as SyncOptions, B as BatchSync, D as Dync, i as SyncState } from '../index.shared-C8JTfet7.cjs';
1
+ import { a as ApiFunctions, h as SyncOptions, B as BatchSync, D as Dync, i as SyncState } from '../index.shared-DHuT0l_t.cjs';
2
2
  import { c as StorageAdapter } from '../dexie-BqktVP7s.cjs';
3
3
  import '../types-CSbIAfu2.cjs';
4
4
  import 'dexie';
@@ -1,4 +1,4 @@
1
- import { a as ApiFunctions, h as SyncOptions, B as BatchSync, D as Dync, i as SyncState } from '../index.shared-DajtjVW7.js';
1
+ import { a as ApiFunctions, h as SyncOptions, B as BatchSync, D as Dync, i as SyncState } from '../index.shared-D-fB8Odd.js';
2
2
  import { c as StorageAdapter } from '../dexie-DRLMKLl5.js';
3
3
  import '../types-CSbIAfu2.js';
4
4
  import 'dexie';
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  Dync
3
- } from "../chunk-6B5N26W3.js";
3
+ } from "../chunk-I4L3W4PX.js";
4
4
  import "../chunk-SQB6E7V2.js";
5
5
 
6
6
  // src/react/useDync.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anfenn/dync",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "private": false,
5
5
  "description": "Write once, run IndexedDB & SQLite with sync anywhere - React, React Native, Expo, Capacitor, Electron & Node.js",
6
6
  "keywords": [
@@ -1,5 +1,15 @@
1
1
  import { deleteKeyIfEmptyObject, omitFields } from '../helpers';
2
- import { LOCAL_PK, UPDATED_AT, SyncAction, type PendingChange, type PersistedSyncState, type SyncState, type SyncStatus } from '../types';
2
+ import {
3
+ LOCAL_PK,
4
+ UPDATED_AT,
5
+ SyncAction,
6
+ ApiError,
7
+ parseApiError,
8
+ type PendingChange,
9
+ type PersistedSyncState,
10
+ type SyncState,
11
+ type SyncStatus,
12
+ } from '../types';
3
13
  import type { StorageAdapter } from '../storage/types';
4
14
 
5
15
  const LOCAL_ONLY_SYNC_FIELDS = [LOCAL_PK, UPDATED_AT];
@@ -28,7 +38,7 @@ export interface StateHelpers {
28
38
  hydrate(): Promise<void>;
29
39
  getState(): PersistedSyncState;
30
40
  setState(setterOrState: PersistedSyncState | ((state: PersistedSyncState) => Partial<PersistedSyncState>)): Promise<void>;
31
- setErrorInMemory(error: Error): void;
41
+ setApiError(error: Error | undefined): void;
32
42
  addPendingChange(change: Omit<PendingChange, 'version'>): Promise<void>;
33
43
  samePendingVersion(tableName: string, localId: string, version: number): boolean;
34
44
  removePendingChange(localId: string, tableName: string): Promise<void>;
@@ -44,6 +54,7 @@ export interface StateHelpers {
44
54
  export class StateManager implements StateHelpers {
45
55
  private persistedState: PersistedSyncState;
46
56
  private syncStatus: SyncStatus;
57
+ private apiError?: ApiError;
47
58
  private readonly listeners = new Set<(state: SyncState) => void>();
48
59
  private readonly storageAdapter?: StorageAdapter;
49
60
  private hydrated = false;
@@ -100,12 +111,8 @@ export class StateManager implements StateHelpers {
100
111
  return this.persist();
101
112
  }
102
113
 
103
- /**
104
- * Set error in memory only without persisting to database.
105
- * Used when the database itself failed to open.
106
- */
107
- setErrorInMemory(error: Error): void {
108
- this.persistedState = { ...this.persistedState, error };
114
+ setApiError(error: Error | undefined): void {
115
+ this.apiError = error ? parseApiError(error) : undefined;
109
116
  this.emit();
110
117
  }
111
118
 
@@ -199,7 +206,7 @@ export class StateManager implements StateHelpers {
199
206
  }
200
207
 
201
208
  getSyncState(): SyncState {
202
- return buildSyncState(this.persistedState, this.syncStatus, this.hydrated);
209
+ return buildSyncState(this.persistedState, this.syncStatus, this.hydrated, this.apiError);
203
210
  }
204
211
 
205
212
  subscribe(listener: (state: SyncState) => void): () => void {
@@ -229,12 +236,13 @@ function resolveNextState(
229
236
  return { ...current, ...setterOrState };
230
237
  }
231
238
 
232
- function buildSyncState(state: PersistedSyncState, status: SyncStatus, hydrated: boolean): SyncState {
239
+ function buildSyncState(state: PersistedSyncState, status: SyncStatus, hydrated: boolean, apiError?: ApiError): SyncState {
233
240
  const persisted = clonePersistedState(state);
234
241
  const syncState: SyncState = {
235
242
  ...persisted,
236
243
  status,
237
244
  hydrated,
245
+ apiError,
238
246
  };
239
247
  deleteKeyIfEmptyObject(syncState, 'conflicts');
240
248
  return syncState;
@@ -358,10 +358,7 @@ class DyncBase<_TStoreMap = Record<string, any>> {
358
358
  }
359
359
 
360
360
  this.syncStatus = 'idle';
361
- await this.state.setState((syncState) => ({
362
- ...syncState,
363
- error: pullResult.error ?? firstPushSyncError,
364
- }));
361
+ this.state.setApiError(pullResult.error ?? firstPushSyncError);
365
362
 
366
363
  if (this.mutationsDuringSync) {
367
364
  this.mutationsDuringSync = false;
package/src/types.ts CHANGED
@@ -5,6 +5,69 @@ export const SERVER_PK = 'id';
5
5
  export const LOCAL_PK = '_localId';
6
6
  export const UPDATED_AT = 'updated_at';
7
7
 
8
+ export class ApiError extends Error {
9
+ readonly isNetworkError: boolean;
10
+ override readonly cause?: Error;
11
+
12
+ constructor(message: string, isNetworkError: boolean, cause?: Error) {
13
+ super(message);
14
+ this.name = 'ApiError';
15
+ this.isNetworkError = isNetworkError;
16
+ this.cause = cause;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Detects if an error is a network-level failure from common HTTP libraries.
22
+ * Supports: fetch, axios, Apollo GraphQL, and generic network errors.
23
+ */
24
+ function isNetworkError(error: Error): boolean {
25
+ const message = error.message.toLowerCase();
26
+ const name = error.name;
27
+
28
+ // fetch: throws TypeError on network failure
29
+ if (name === 'TypeError' && (message.includes('failed to fetch') || message.includes('network request failed'))) {
30
+ return true;
31
+ }
32
+
33
+ // axios: sets error.code for network issues
34
+ const code = (error as any).code;
35
+ if (code === 'ERR_NETWORK' || code === 'ECONNABORTED' || code === 'ENOTFOUND' || code === 'ECONNREFUSED') {
36
+ return true;
37
+ }
38
+
39
+ // axios: no response means request never reached server
40
+ if ((error as any).isAxiosError && (error as any).response === undefined) {
41
+ return true;
42
+ }
43
+
44
+ // Apollo GraphQL: network error wrapper
45
+ if (name === 'ApolloError' && (error as any).networkError) {
46
+ return true;
47
+ }
48
+
49
+ // Generic network error messages
50
+ if (message.includes('network error') || message.includes('networkerror')) {
51
+ return true;
52
+ }
53
+
54
+ return false;
55
+ }
56
+
57
+ export function parseApiError(error: unknown): ApiError {
58
+ if (error instanceof ApiError) {
59
+ return error;
60
+ }
61
+
62
+ if (error instanceof Error) {
63
+ return new ApiError(error.message, isNetworkError(error), error);
64
+ }
65
+
66
+ // Non-Error thrown (string, object, etc.)
67
+ const message = String(error);
68
+ return new ApiError(message, false);
69
+ }
70
+
8
71
  export interface SyncedRecord {
9
72
  _localId: string;
10
73
  id?: any;
@@ -138,7 +201,6 @@ export interface PersistedSyncState {
138
201
  firstLoadDone: boolean;
139
202
  pendingChanges: PendingChange[];
140
203
  lastPulled: Record<string, string>;
141
- error?: Error;
142
204
  conflicts?: Record<string, Conflict>;
143
205
  }
144
206
 
@@ -147,6 +209,7 @@ export type SyncStatus = 'disabled' | 'disabling' | 'idle' | 'syncing' | 'error'
147
209
  export interface SyncState extends PersistedSyncState {
148
210
  status: SyncStatus;
149
211
  hydrated: boolean;
212
+ apiError?: ApiError;
150
213
  }
151
214
 
152
215
  export enum SyncAction {