Skip to content

Commit

Permalink
Add a revert function to onError in set functions
Browse files Browse the repository at this point in the history
  • Loading branch information
jmeistrich committed Nov 2, 2024
1 parent 998fd7b commit e99eadf
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/sync-plugins/crud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export interface WaitForSetCrudFnParams<T> extends WaitForSetFnParams<T> {
}

export interface CrudErrorParams extends Omit<SyncedErrorParams, 'source'> {
source: 'list' | 'get' | 'create' | 'update' | 'delete';
source: 'list' | 'get' | 'create' | 'update' | 'delete' | 'unknown';
}

export type CrudOnErrorFn = (error: Error, params: CrudErrorParams) => void;
Expand Down
9 changes: 5 additions & 4 deletions src/sync-plugins/keel.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { batch, isEmpty, isFunction, observable, when } from '@legendapp/state';
import {
SyncedErrorParams,
SyncedGetSetSubscribeBaseParams,
createRevertChanges,
type SyncedGetParams,
type SyncedGetSetSubscribeBaseParams,
type SyncedSetParams,
type SyncedSubscribeParams,
} from '@legendapp/state/sync';
import {
CrudAsOption,
CrudErrorParams,
CrudResult,
SyncedCrudOnSavedParams,
SyncedCrudPropsBase,
Expand Down Expand Up @@ -138,8 +139,7 @@ interface SyncedKeelPropsSingle<TRemote extends { id: string }, TLocal>
as?: never;
}

export interface KeelErrorParams extends Omit<SyncedErrorParams, 'source'> {
source: 'list' | 'get' | 'create' | 'update' | 'delete';
export interface KeelErrorParams extends CrudErrorParams {
action: string;
}

Expand Down Expand Up @@ -489,6 +489,7 @@ export function syncedKeel<
source: from,
action: fn.name || fn.toString(),
retry: params,
revert: createRevertChanges(params.value$, params.changes),
});
}
}
Expand Down
17 changes: 11 additions & 6 deletions src/sync-plugins/supabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import {
SyncedOptionsGlobal,
SyncedSetParams,
combineTransforms,
createRevertChanges,
removeNullUndefined,
transformStringifyDates,
type SyncedGetParams,
type SyncedSubscribeParams,
} from '@legendapp/state/sync';
import {
CrudAsOption,
CrudErrorParams,
CrudOnErrorFn,
SyncedCrudPropsBase,
SyncedCrudPropsMany,
Expand Down Expand Up @@ -128,14 +130,14 @@ export function configureSyncedSupabase(config: SyncedSupabaseConfiguration) {
Object.assign(supabaseConfig, removeNullUndefined(rest));
}

function wrapSupabaseFn(fn: (...args: any) => PromiseLike<any>) {
function wrapSupabaseFn(fn: (...args: any) => PromiseLike<any>, source: CrudErrorParams['source']) {
return async (params: SyncedGetParams<any>, ...args: any) => {
const { onError } = params;
const { data, error } = await fn(params, ...args);
if (error) {
(onError as CrudOnErrorFn)(new Error(error.message), {
getParams: params,
source: 'list',
source,
type: 'get',
retry: params,
});
Expand Down Expand Up @@ -200,7 +202,7 @@ export function syncedSupabase<
const list =
!actions || actions.includes('read')
? listParam
? wrapSupabaseFn(listParam)
? wrapSupabaseFn(listParam, 'list')
: async (params: SyncedGetParams<TRemote>) => {
const { lastSync, onError } = params;
const clientSchema = schema ? client.schema(schema as string) : client;
Expand Down Expand Up @@ -232,7 +234,7 @@ export function syncedSupabase<
: undefined;

const create = createParam
? wrapSupabaseFn(createParam)
? wrapSupabaseFn(createParam, 'create')
: !actions || actions.includes('create')
? async (input: SupabaseRowOf<Client, Collection, SchemaName>, params: SyncedSetParams<TRemote>) => {
const { onError } = params;
Expand All @@ -248,6 +250,7 @@ export function syncedSupabase<
type: 'set',
retry: params,
input,
revert: createRevertChanges(params.value$, params.changes),
});
}
}
Expand All @@ -256,7 +259,7 @@ export function syncedSupabase<
const update =
!actions || actions.includes('update')
? updateParam
? wrapSupabaseFn(updateParam)
? wrapSupabaseFn(updateParam, 'update')
: async (input: SupabaseRowOf<Client, Collection, SchemaName>, params: SyncedSetParams<TRemote>) => {
const { onError } = params;
const res = await client.from(collection).update(input).eq('id', input.id).select();
Expand All @@ -271,6 +274,7 @@ export function syncedSupabase<
type: 'set',
retry: params,
input,
revert: createRevertChanges(params.value$, params.changes),
});
}
}
Expand All @@ -279,7 +283,7 @@ export function syncedSupabase<
const deleteFn =
!fieldDeleted && (!actions || actions.includes('delete'))
? deleteParam
? wrapSupabaseFn(deleteParam)
? wrapSupabaseFn(deleteParam, 'delete')
: async (
input: { id: SupabaseRowOf<Client, Collection, SchemaName>['id'] },
params: SyncedSetParams<TRemote>,
Expand All @@ -298,6 +302,7 @@ export function syncedSupabase<
type: 'set',
retry: params,
input,
revert: createRevertChanges(params.value$, params.changes),
});
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/sync/revertChanges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { applyChanges, Change, internal, ObservableParam } from '@legendapp/state';
import { onChangeRemote } from '@legendapp/state/sync';

const { clone } = internal;

export function createRevertChanges(obs$: ObservableParam<any>, changes: Change[]) {
return () => {
const previous = applyChanges(clone(obs$.peek()), changes, /*applyPrevious*/ true);
onChangeRemote(() => {
obs$.set(previous);
});
};
}
4 changes: 3 additions & 1 deletion src/sync/syncObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import type {
SyncedSubscribeParams,
} from './syncTypes';
import { waitForSet } from './waitForSet';
import { createRevertChanges } from './revertChanges';

const { clone, deepMerge, getNode, getNodeValue, getValueAtPath, globalState, symbolLinked, createPreviousHandler } =
internal;
Expand Down Expand Up @@ -639,10 +640,11 @@ async function doChangeRemote(changeInfo: PreppedChangeRemote | undefined) {
type: 'set',
input: value,
retry: setParams,
revert: createRevertChanges(setParams.value$, setParams.changes),
};
}
state$.error.set(error);
syncOptions.onError?.(error, params);
syncOptions.onError?.(error, params!);
lastErrorHandled = error;
if (!noThrow) {
throw error;
Expand Down
1 change: 1 addition & 0 deletions src/sync/syncTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export interface SyncedErrorParams {
setParams?: SyncedSetParams<any>;
subscribeParams?: SyncedSubscribeParams<any>;
input?: any;
revert?: () => void;
}

export interface SyncedOptions<TRemote = any, TLocal = TRemote> extends Omit<LinkedOptions<TRemote>, 'get' | 'set'> {
Expand Down
1 change: 1 addition & 0 deletions sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { mapSyncPlugins, onChangeRemote, syncObservable } from './src/sync/syncO
export * from './src/sync/syncTypes';
export { synced } from './src/sync/synced';
export * from './src/sync/configureSynced';
export { createRevertChanges } from './src/sync/revertChanges';

import { waitForSet } from './src/sync/waitForSet';
import { observableSyncConfiguration } from './src/sync/configureObservableSync';
Expand Down
33 changes: 33 additions & 0 deletions tests/crud.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2917,6 +2917,39 @@ describe('Error is set', () => {
expect(errorAtOnError).toEqual(new Error('test'));
expect(numErrors).toEqual(1);
});
test('onError can revert if create fails', async () => {
let errorAtOnError: Error | undefined = undefined;
let numErrors = 0;
const obs$ = observable(
syncedCrud({
list: () => promiseTimeout(0, [ItemBasicValue()]),
as: 'object',
create: async () => {
throw new Error('test');
},
onError: (error, params) => {
numErrors++;
errorAtOnError = error;
params.revert!();
},
}),
);
expectTypeOf<(typeof obs$)['get']>().returns.toEqualTypeOf<Record<string, BasicValue>>();

expect(obs$.get()).toEqual(undefined);

await promiseTimeout(1);

expect(obs$.get()).toEqual({ id1: { id: 'id1', test: 'hi' } });
obs$.id2.set({ id: 'id2', test: 'hi' });

await promiseTimeout(1);

expect(errorAtOnError).toEqual(new Error('test'));
expect(numErrors).toEqual(1);

expect(obs$.get()).toEqual({ id1: { id: 'id1', test: 'hi' }, id2: undefined });
});
test('onError is called if list fails', async () => {
let errorAtOnError: Error | undefined = undefined;
let numErrors = 0;
Expand Down

0 comments on commit e99eadf

Please sign in to comment.