From da9527c0c6a328440988adf8666f81e02d50aba7 Mon Sep 17 00:00:00 2001 From: senelway Date: Sat, 10 Jan 2026 11:50:20 +0000 Subject: [PATCH 1/2] [beta.9] feat: skip strategy --- README.md | 6 ++- lib/reducer/index.ts | 2 +- lib/reducer/storage.ts | 8 +++ lib/reducer/types.ts | 2 +- package.json | 2 +- tests/lib/reducer/storage.test.ts | 86 +++++++++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d1984fb..49559eb 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ interface CdeebeeSettings { modules: CdeebeeModule[]; // Active modules: 'history' | 'listener' | 'storage' | 'cancelation' | 'queryQueue' fileKey: string; // Key name for file uploads in FormData bodyKey: string; // Key name for request body in FormData - listStrategy?: CdeebeeListStrategy; // Merge strategy per list: 'merge' | 'replace' + listStrategy?: CdeebeeListStrategy; // Merge strategy per list: 'merge' | 'replace' | 'skip' mergeWithData?: unknown; // Data to merge with every request body mergeWithHeaders?: Record; // Headers to merge with every request normalize?: (storage, result, strategyList) => T; // Custom normalization function @@ -198,15 +198,17 @@ interface CdeebeeRequestOptions { ## Data Merging Strategies -cdeebee supports two strategies for merging data: +cdeebee supports three strategies for merging data: - **`merge`**: Merges new data with existing data, preserving existing keys not in the response - **`replace`**: Completely replaces the list with new data +- **`skip`**: Skips updating the list entirely, preserving existing data unchanged ```typescript listStrategy: { forumList: 'merge', // New forums are merged with existing ones threadList: 'replace', // Thread list is completely replaced + userList: 'skip', // User list is never updated, existing data is preserved } ``` diff --git a/lib/reducer/index.ts b/lib/reducer/index.ts index 6079e7d..2e43b28 100644 --- a/lib/reducer/index.ts +++ b/lib/reducer/index.ts @@ -71,7 +71,7 @@ export const factory = (settings: CdeebeeSettings, storage?: T) => { // Type assertion is safe here because we've already checked isRecord const normalizedData = normalize(currentState, action.payload.result as Record>, strategyList); - // Normalize already handles merge/replace and preserves keys not in response + // Normalize already handles merge/replace/skip and preserves keys not in response // Simply apply the result // eslint-disable-next-line @typescript-eslint/no-explicit-any (state.storage as any) = normalizedData; diff --git a/lib/reducer/storage.ts b/lib/reducer/storage.ts index 0bf5b26..829c4f1 100644 --- a/lib/reducer/storage.ts +++ b/lib/reducer/storage.ts @@ -37,6 +37,8 @@ function applyStrategy( return newValue as ResponseValue; } else if (strategy === 'merge') { return mergeDeepRight(existingValue, newValue as StorageData) as ResponseValue; + } else if (strategy === 'skip') { + return existingValue as ResponseValue; } else { console.warn(`Cdeebee: Unknown strategy "${strategy}" for key "${key}". Skipping normalization.`); return mergeDeepRight(existingValue, newValue as StorageData) as ResponseValue; @@ -63,6 +65,12 @@ export function defaultNormalize( } const strategy = strategyList[key as keyof T] ?? 'merge'; + + // For 'skip' strategy, if key doesn't exist in storage, skip it entirely + if (strategy === 'skip' && !(key in currentStorage)) { + continue; + } + const existingValue = key in currentStorage ? (currentStorage[key] as StorageData) : {}; if (isDataWithPrimaryKey(responseValue)) { diff --git a/lib/reducer/types.ts b/lib/reducer/types.ts index 04351e1..1af699e 100644 --- a/lib/reducer/types.ts +++ b/lib/reducer/types.ts @@ -1,5 +1,5 @@ export type CdeebeeModule = 'history' | 'listener' | 'storage' | 'cancelation' | 'queryQueue'; -export type CdeebeeStrategy = 'merge' | 'replace'; +export type CdeebeeStrategy = 'merge' | 'replace' | 'skip'; export type CdeebeeListStrategy = Record; diff --git a/package.json b/package.json index 55060ff..7494887 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@recats/cdeebee", - "version": "3.0.0-beta.8", + "version": "3.0.0-beta.9", "description": "React Redux data-logic library", "repository": "git@github.com:recats/cdeebee.git", "author": "recats", diff --git a/tests/lib/reducer/storage.test.ts b/tests/lib/reducer/storage.test.ts index 54b840a..65e2575 100644 --- a/tests/lib/reducer/storage.test.ts +++ b/tests/lib/reducer/storage.test.ts @@ -148,6 +148,92 @@ describe('defaultNormalize', () => { }); }); + describe('skip strategy', () => { + it('should preserve existing data unchanged when skip strategy is used', () => { + mockCdeebee.storage = { + userList: { + '1': { id: '1', name: 'John', age: 30 }, + '3': { id: '3', name: 'Bob' }, + }, + }; + + const response = { + userList: { + data: [ + { id: '1', name: 'John Updated', age: 31 }, + { id: '2', name: 'Jane' }, + ], + primaryKey: 'id', + }, + } as unknown as IResponse; + + const strategyList: CdeebeeListStrategy = { + userList: 'skip', + }; + + const result = defaultNormalize(mockCdeebee, response, strategyList); + + // Existing data should remain unchanged + expect(result.userList).toEqual({ + '1': { id: '1', name: 'John', age: 30 }, + '3': { id: '3', name: 'Bob' }, + }); + // New data from response should be ignored + expect(result.userList).not.toHaveProperty('2'); + }); + + it('should not create new key when skip strategy is used and key does not exist', () => { + const response = { + userList: { + data: [ + { id: '1', name: 'John' }, + { id: '2', name: 'Jane' }, + ], + primaryKey: 'id', + }, + } as unknown as IResponse; + + const strategyList: CdeebeeListStrategy = { + userList: 'skip', + }; + + const result = defaultNormalize(mockCdeebee, response, strategyList); + + // Key should not be created + expect(result).not.toHaveProperty('userList'); + }); + + it('should preserve existing data when skip strategy is used with record format', () => { + mockCdeebee.storage = { + userList: { + '1': { id: '1', name: 'John', age: 30 }, + '3': { id: '3', name: 'Bob' }, + }, + }; + + const response = { + userList: { + '1': { id: '1', name: 'John Updated', age: 31 }, + '2': { id: '2', name: 'Jane' }, + }, + } as unknown as IResponse; + + const strategyList: CdeebeeListStrategy = { + userList: 'skip', + }; + + const result = defaultNormalize(mockCdeebee, response, strategyList); + + // Existing data should remain unchanged + expect(result.userList).toEqual({ + '1': { id: '1', name: 'John', age: 30 }, + '3': { id: '3', name: 'Bob' }, + }); + // New data from response should be ignored + expect(result.userList).not.toHaveProperty('2'); + }); + }); + describe('unknown strategy', () => { it('should fall back to merge strategy for unknown strategy', () => { const response = { From 56c77a5e94ebe0f44b2a897c32b0cbd6c4824abc Mon Sep 17 00:00:00 2001 From: senelway Date: Sat, 10 Jan 2026 11:52:06 +0000 Subject: [PATCH 2/2] add export --- lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/index.ts b/lib/index.ts index c6c7eba..c099310 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,4 +1,4 @@ -export { type CdeebeeState, type CdeebeeValueList, type CdeebeeRequestOptions, type CdeebeeModule } from './reducer/types'; +export { type CdeebeeListStrategy, type CdeebeeState, type CdeebeeValueList, type CdeebeeRequestOptions, type CdeebeeModule } from './reducer/types'; export { batchingUpdate } from './reducer/helpers'; export { request } from './reducer/request'; export { factory } from './reducer/index';