@anfenn/dync 1.0.14 → 1.0.16

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-NJF2KCLA.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.14",
3
+ "version": "1.0.16",
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,5 @@
1
- import { deleteKeyIfEmptyObject, omitFields } from '../helpers';
2
- import { LOCAL_PK, UPDATED_AT, SyncAction, type PendingChange, type PersistedSyncState, type SyncState, type SyncStatus } from '../types';
1
+ import { deleteKeyIfEmptyObject, omitFields, parseApiError } from '../helpers';
2
+ import { LOCAL_PK, UPDATED_AT, SyncAction, ApiError, type PendingChange, type PersistedSyncState, type SyncState, type SyncStatus } from '../types';
3
3
  import type { StorageAdapter } from '../storage/types';
4
4
 
5
5
  const LOCAL_ONLY_SYNC_FIELDS = [LOCAL_PK, UPDATED_AT];
@@ -28,7 +28,7 @@ export interface StateHelpers {
28
28
  hydrate(): Promise<void>;
29
29
  getState(): PersistedSyncState;
30
30
  setState(setterOrState: PersistedSyncState | ((state: PersistedSyncState) => Partial<PersistedSyncState>)): Promise<void>;
31
- setErrorInMemory(error: Error): void;
31
+ setApiError(error: Error | undefined): void;
32
32
  addPendingChange(change: Omit<PendingChange, 'version'>): Promise<void>;
33
33
  samePendingVersion(tableName: string, localId: string, version: number): boolean;
34
34
  removePendingChange(localId: string, tableName: string): Promise<void>;
@@ -44,6 +44,7 @@ export interface StateHelpers {
44
44
  export class StateManager implements StateHelpers {
45
45
  private persistedState: PersistedSyncState;
46
46
  private syncStatus: SyncStatus;
47
+ private apiError?: ApiError;
47
48
  private readonly listeners = new Set<(state: SyncState) => void>();
48
49
  private readonly storageAdapter?: StorageAdapter;
49
50
  private hydrated = false;
@@ -100,12 +101,8 @@ export class StateManager implements StateHelpers {
100
101
  return this.persist();
101
102
  }
102
103
 
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 };
104
+ setApiError(error: Error | undefined): void {
105
+ this.apiError = error ? parseApiError(error) : undefined;
109
106
  this.emit();
110
107
  }
111
108
 
@@ -199,7 +196,7 @@ export class StateManager implements StateHelpers {
199
196
  }
200
197
 
201
198
  getSyncState(): SyncState {
202
- return buildSyncState(this.persistedState, this.syncStatus, this.hydrated);
199
+ return buildSyncState(this.persistedState, this.syncStatus, this.hydrated, this.apiError);
203
200
  }
204
201
 
205
202
  subscribe(listener: (state: SyncState) => void): () => void {
@@ -229,12 +226,13 @@ function resolveNextState(
229
226
  return { ...current, ...setterOrState };
230
227
  }
231
228
 
232
- function buildSyncState(state: PersistedSyncState, status: SyncStatus, hydrated: boolean): SyncState {
229
+ function buildSyncState(state: PersistedSyncState, status: SyncStatus, hydrated: boolean, apiError?: ApiError): SyncState {
233
230
  const persisted = clonePersistedState(state);
234
231
  const syncState: SyncState = {
235
232
  ...persisted,
236
233
  status,
237
234
  hydrated,
235
+ apiError,
238
236
  };
239
237
  deleteKeyIfEmptyObject(syncState, 'conflicts');
240
238
  return syncState;
package/src/helpers.ts CHANGED
@@ -1,8 +1,57 @@
1
- import { SyncAction } from './types';
1
+ import { ApiError, SyncAction } from './types';
2
2
  import { createLocalId } from './createLocalId';
3
3
 
4
4
  export { createLocalId };
5
5
 
6
+ export function parseApiError(error: any): ApiError {
7
+ if (error instanceof ApiError) {
8
+ return error;
9
+ }
10
+
11
+ if (typeof error === 'string') {
12
+ return new ApiError(error, false);
13
+ }
14
+
15
+ return new ApiError(error.message, isNetworkError(error), error);
16
+ }
17
+
18
+ /**
19
+ * Detects if an error is a network-level failure from common HTTP libraries.
20
+ * Supports: fetch, axios, Apollo GraphQL, and generic network errors.
21
+ */
22
+ function isNetworkError(error: any): boolean {
23
+ const message = error.message?.toLowerCase() ?? '';
24
+ const name = error.name;
25
+
26
+ // fetch: throws TypeError on network failure
27
+ if (name === 'TypeError' && (message.includes('failed to fetch') || message.includes('network request failed'))) {
28
+ return true;
29
+ }
30
+
31
+ // axios: sets error.code for network issues
32
+ const code = error.code;
33
+ if (code === 'ERR_NETWORK' || code === 'ECONNABORTED' || code === 'ENOTFOUND' || code === 'ECONNREFUSED') {
34
+ return true;
35
+ }
36
+
37
+ // axios: no response means request never reached server
38
+ if (error.isAxiosError && error.response === undefined) {
39
+ return true;
40
+ }
41
+
42
+ // Apollo GraphQL: network error wrapper
43
+ if (name === 'ApolloError' && error.networkError) {
44
+ return true;
45
+ }
46
+
47
+ // Generic network error messages
48
+ if (message.includes('network error') || message.includes('networkerror')) {
49
+ return true;
50
+ }
51
+
52
+ return false;
53
+ }
54
+
6
55
  export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
7
56
  return new Promise((resolve) => {
8
57
  if (signal?.aborted) {
@@ -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,18 @@ 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
+
8
20
  export interface SyncedRecord {
9
21
  _localId: string;
10
22
  id?: any;
@@ -138,7 +150,6 @@ export interface PersistedSyncState {
138
150
  firstLoadDone: boolean;
139
151
  pendingChanges: PendingChange[];
140
152
  lastPulled: Record<string, string>;
141
- error?: Error;
142
153
  conflicts?: Record<string, Conflict>;
143
154
  }
144
155
 
@@ -147,6 +158,7 @@ export type SyncStatus = 'disabled' | 'disabling' | 'idle' | 'syncing' | 'error'
147
158
  export interface SyncState extends PersistedSyncState {
148
159
  status: SyncStatus;
149
160
  hydrated: boolean;
161
+ apiError?: ApiError;
150
162
  }
151
163
 
152
164
  export enum SyncAction {