Skip to content

Instantly share code, notes, and snippets.

@enjoylife
Last active September 28, 2019 05:48
Show Gist options
  • Save enjoylife/024012d46f9749bb1227f0b93a36b843 to your computer and use it in GitHub Desktop.
Save enjoylife/024012d46f9749bb1227f0b93a36b843 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
const resetableEditableContext = () => {
return {
pushing: {
updates: [],
error: null,
lastSuccess: null,
},
pulling: {
error: null,
lastSuccess: null,
updates: [],
},
};
};
const editableMachineError = key => {
return assign({
[key]: (context, event) => {
// using an immutable pattern
return {
...context[key],
error: event.error,
};
},
});
};
const editableMachineSuccess = key => {
return assign({
[key]: (context, event) => {
// using an immutable pattern
return {
...context[key],
lastSuccess: new Date().getTime(),
error: null,
updates: [],
};
},
});
};
function fakeSyncFetch(changes) {
return new Promise((resolve, reject) => {
console.log('fake sync fetch', changes);
if (Math.random() >= 0.8) {
setTimeout(resolve, 5000, {accepted: changes});
} else {
setTimeout(reject, 2000, {auth: 'not allowed'});
}
});
}
const simpleSyncLoop = (callback, onEvent) => {
const changeHandler = changes => fakeSyncFetch(changes);
console.log('syncLoop started');
// Receive events from parent
onEvent(event => {
console.log('syncLoop recieved event', event);
if (event.type === 'CHANGE') {
event.change = 'x'; // for playground
console.log('queuing change', event.change);
const onSynced = data => {
console.log('promise done!');
callback({
type: 'SUCCESS',
syncResponse: data,
});
};
const onError = e => {
console.log('promise failed!');
callback({
type: 'ERROR',
error: e,
});
};
// We have changes we need to sync...
changeHandler(event.change)
.then(onSynced)
.catch(onError);
}
});
// Perform cleanup
return () => {
// TODO cancel any debounced/pending changes
console.log('cleaned up');
};
};
// A editableMachine provides a state machine to manage a component which
// can take on a viewable or editable state. More importantly when
// edits do occur the process and constraints to make sure no changes are lost are provided by the state machine.
const editableMachine = Machine(
{
id: 'editable',
type: 'parallel',
strict: true,
context: {
canEdit: true,
retryOptions: {
pushing: {}, // leverages defaults of retry function
pulling: {}, // leverages defaults of retry
},
...resetableEditableContext(),
},
states: {
editor: {
initial: 'viewing',
states: {
viewing: {
on: {EDIT: {target: 'editing', cond: 'canEdit'}},
},
editing: {
on: {
DONE: [
{target: 'viewing', cond: 'isSaved' /*actions: 'flush', */},
{target: 'prompt_force_view', cond: 'waitingOnSave'},
],
},
},
printing: {
on: {
DONE: {target: 'viewing'},
},
},
prompt_force_view: {
exit: ['resetContext'],
on: {
DONE: {target: 'viewing'},
},
},
},
},
syncer: {
type: 'parallel',
states: {
// pushing is the states which deal with
// edits originating from the user or client side
// which need to be pushed out to the server
pushing: {
initial: 'sync',
states: {
idle: {
on: {
SYNC: {target: 'sync'},
CHANGE: {
actions: ['queueChange'],
},
},
},
sync: {
invoke: {
id: 'syncLoop',
autoForward: true,
src: ['syncLoopFn'],
// error in callback prior to any sending..
onError: {
target: 'idle',
actions: [
'sendChange',
assign({
pushing: (context, event) => {
console.log('error in syncLoop');
// using an immutable pattern
return {
...context.pushing,
error: event.data,
};
},
}),
],
},
},
on: {
CHANGE: {
target: 'sync',
actions: ['queueChange', 'sendChange'],
},
PAUSE: {target: 'idle'},
SUCCESS: {target: 'sync', actions: ['pushingSuccess']},
FAILURE: {target: 'idle', actions: ['pushingError']},
},
},
},
},
pulling: {
initial: 'idle',
states: {
idle: {
on: {
START: {target: 'sync'},
},
},
sync: {
on: {
PAUSE: {target: 'idle'},
SUCCESS: {target: 'sync', actions: ['pullingSuccess']},
FAILURE: {target: 'idle', actions: ['pullingError']},
},
},
},
},
},
// invoke: {
// id: 'syncMachine',
// src: syncMachine,
// // Deriving child context from parent context
// data: {
// duration: (context, event) => context.customDuration
// }
// }
},
// TODO: state waiting for confirmation that FORCE_DONE is ok
},
},
{
actions: {
resetContext: assign({
...resetableEditableContext(),
}),
pushingError: editableMachineError('pushing'),
pullingError: editableMachineError('pulling'),
pushingSuccess: editableMachineSuccess('pushing'),
pullingSuccess: editableMachineSuccess('pulling'),
queueChange: assign({
pushing: (context, event) => {
// using an immutable pattern
return {
...context.pushing,
updates: [...context.pushing.updates, event.change],
};
},
}),
sendChange: send(
(context, event) => {
return {
type: 'CHANGE',
change: event.change,
};
},
{to: 'syncLoop'}
),
},
guards: {
canEdit: (context, event) => context.canEdit,
isSaved: (context, event) =>
context.pushing.updates.length === 0 && !context.error,
waitingOnSave: (context, event) =>
context.pushing.updates.length !== 0 &&
!context.pushing.error &&
!context.pulling.error,
},
services: {
syncLoopFn: simpleSyncLoop,
},
}
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment