Created
February 29, 2020 23:22
-
-
Save sarimarton/8b4dab5f474bdeb43bd2a0143662b3ab to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// types.ts | |
import { AutoSaveEvent } from 'src/store/slices/autoSave' | |
import { EntityUpdateEvent } from 'src/store/slices/review' | |
import { StateMachine } from 'xstate' | |
export type Machine = StateMachine<MachineContext, MachineStateSchema, MachineEvent> | |
export type MachineEvent = | |
| EntityUpdateEvent | |
| { type: 'UPDATED' } | |
| { type: 'TICK' } | |
| AutoSaveEvent | |
export type MachineContext = { | |
revId: number | |
dirtyKeys: string[] | |
pendingKeys: string[] | |
errorRetryLevels: [10, 30, 300] | |
errorRetryLevelIdx: number | |
errorCounter: number | |
} | |
export interface MachineStateSchema { | |
states: { | |
idle: {} | |
saving: { | |
states: { | |
request: { | |
states: { | |
pending: {} | |
completed: {} | |
} | |
} | |
debounce: { | |
states: { | |
debouncing: {} | |
debouncerestart: {} | |
settled: {} | |
} | |
} | |
} | |
} | |
serviceerror: { | |
states: { | |
counting: {} | |
} | |
} | |
} | |
} | |
// machine.ts | |
import { Machine, assign, send } from 'xstate' | |
import { MachineContext, MachineStateSchema, MachineEvent } from './types' | |
export default Machine<MachineContext, MachineStateSchema, MachineEvent>( | |
// Below this it's copy-pasteable into the visualizer | |
{ | |
id: 'autosave', | |
initial: 'idle', | |
context: { | |
revId: 0, | |
dirtyKeys: [], | |
pendingKeys: [], | |
errorRetryLevels: [10, 30, 300], | |
errorRetryLevelIdx: 0, | |
errorCounter: 0 | |
}, | |
on: { | |
'nike/review/UPDATE_ENTITY': { | |
actions: ['updateDirtyKeys', 'forwardUpdateEventToSubstates'] | |
} | |
}, | |
states: { | |
idle: { | |
on: { | |
UPDATED: 'saving' | |
} | |
}, | |
saving: { | |
type: 'parallel', | |
entry: 'incrementRevision', | |
states: { | |
request: { | |
states: { | |
pending: {}, | |
completed: {} | |
}, | |
initial: 'pending', | |
entry: 'moveDirtyKeysToPending', | |
invoke: { | |
id: 'request', | |
src: 'save', | |
onDone: { | |
target: '.completed', | |
actions: ['resetRetryInterval', 'clearPendingKeys'] | |
}, | |
onError: { | |
target: '#autosave.serviceerror', | |
actions: 'putBackPendingKeys' | |
} | |
} | |
}, | |
debounce: { | |
states: { | |
debouncing: { | |
invoke: { | |
id: 'debounce', | |
src: 'debounce', | |
onDone: 'settled' | |
} | |
}, | |
debouncerestart: { | |
on: { | |
'': 'debouncing' | |
} | |
}, | |
settled: {} | |
}, | |
initial: 'debouncing', | |
on: { | |
UPDATED: '.debouncerestart' | |
} | |
} | |
}, | |
on: { | |
'': [ | |
{ | |
target: 'idle', | |
cond: 'saveFinished' | |
}, | |
{ | |
target: 'saving', | |
cond: 'saveFinishedDirty' | |
} | |
] | |
} | |
}, | |
serviceerror: { | |
id: 'serviceerror', | |
invoke: { | |
id: 'timer', | |
src: 'timer' | |
}, | |
states: { | |
counting: { | |
entry: 'initServiceErrorCounter', | |
on: { | |
TICK: { | |
actions: 'decrementCounter' | |
}, | |
'nike/autoSave/FORCE_RETRY': { | |
target: '#autosave.saving', | |
actions: 'resetRetryInterval' | |
}, | |
'': { | |
cond: 'retryCounterTimeup', | |
actions: 'levelUpRetryInterval', | |
target: '#autosave.saving' | |
} | |
} | |
} | |
}, | |
initial: 'counting' | |
} | |
} | |
}, | |
{ | |
actions: { | |
forwardUpdateEventToSubstates: send('UPDATED'), | |
updateDirtyKeys: assign({ | |
dirtyKeys: (ctx, event) => { | |
// Not sure how to avoid this without putting TS syntax | |
// in the signature of this function | |
// @ts-ignore | |
const key = JSON.stringify([event.entityId, event.dataType]) | |
return [...new Set([...ctx.dirtyKeys, key])] | |
} | |
}), | |
moveDirtyKeysToPending: assign(ctx => ({ | |
pendingKeys: ctx.dirtyKeys, | |
dirtyKeys: [] | |
})), | |
putBackPendingKeys: assign(ctx => ({ | |
dirtyKeys: [...new Set([...ctx.dirtyKeys, ...ctx.pendingKeys])], | |
pendingKeys: [] | |
})), | |
clearPendingKeys: assign(ctx => ({ | |
pendingKeys: [] | |
})), | |
incrementRevision: assign({ | |
revId: ctx => ctx.revId + 1 | |
}), | |
decrementCounter: assign({ | |
errorCounter: ctx => ctx.errorCounter - 1 | |
}), | |
initServiceErrorCounter: assign({ | |
errorCounter: ctx => ctx.errorRetryLevels[ctx.errorRetryLevelIdx] | |
}), | |
resetRetryInterval: assign({ | |
errorRetryLevelIdx: ctx => 0 | |
}), | |
levelUpRetryInterval: assign({ | |
errorRetryLevelIdx: ctx => | |
Math.min(ctx.errorRetryLevelIdx + 1, ctx.errorRetryLevels.length - 1) | |
}) | |
}, | |
guards: { | |
saveFinished: (ctx, event, meta) => | |
!ctx.dirtyKeys.length && | |
meta.state && // <- this for the visualizer | |
meta.state.matches({ | |
saving: { | |
request: 'completed', | |
debounce: 'settled' | |
} | |
}), | |
saveFinishedDirty: (ctx, event, meta) => | |
ctx.dirtyKeys.length && | |
meta.state && // <- this for the visualizer | |
meta.state.matches({ | |
saving: { | |
request: 'completed', | |
debounce: 'settled' | |
} | |
}), | |
retryCounterTimeup: ctx => ctx.errorCounter === 0 | |
}, | |
services: { | |
// save: ctx => | |
// new Promise((resolve, reject) => { | |
// setTimeout(resolve, 2000) | |
// }), | |
save: context => { | |
// return save(context.revId, context.pendingKeys, store.getState().document) | |
console.log( | |
'saving', | |
context.revId, | |
context.pendingKeys | |
) | |
return new Promise((resolve, reject) => { | |
setTimeout(reject, 2000) | |
}) | |
}, | |
debounce: () => | |
new Promise(resolve => { | |
setTimeout(resolve, 500) | |
}), | |
timer: () => callback => { | |
const id = setInterval(() => { | |
callback('TICK') | |
}, 1000) | |
return () => clearInterval(id) | |
} | |
} | |
} | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment