Skip to content

Instantly share code, notes, and snippets.

@sarimarton
Created February 29, 2020 23:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sarimarton/8b4dab5f474bdeb43bd2a0143662b3ab to your computer and use it in GitHub Desktop.
Save sarimarton/8b4dab5f474bdeb43bd2a0143662b3ab to your computer and use it in GitHub Desktop.
// 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