Last active
November 22, 2019 13:20
-
-
Save mr-mig/57d0f3da972f353fa57b488fdc6f3464 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
This file contains 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
// https://xstate.js.org/viz/?gist=57d0f3da972f353fa57b488fdc6f3464 | |
// Available variables: | |
// - Machine | |
// - interpret | |
// - assign | |
// - send | |
// - sendParent | |
// - spawn | |
// - raise | |
// - actions | |
// - XState (all XState exports) | |
const onlineChecker = Machine( | |
{ | |
id: 'onlineChecker', | |
context: { | |
onlineCheckInterval: 30 * 1000, | |
remoteOnlineCheckURL: | |
'https://to-do-cdn.microsoft.com/static-assets/online.txt' | |
}, | |
initial: 'init', | |
states: { | |
init: { | |
on: { | |
CHECK_REMOTE: '.checkingConnectivity' | |
}, | |
invoke: { | |
src: 'offlineListener' | |
}, | |
initial: 'checkingConnectivity', | |
states: { | |
checkingConnectivity: { | |
on: { | |
ONLINE: 'online', | |
OFFLINE: 'offline' | |
}, | |
invoke: { | |
src: 'checkRemote', | |
onDone: 'online', | |
onError: { | |
actions: [send('OFFLINE'), sendParent('OFFLINE')] | |
} | |
}, | |
}, | |
online: { | |
entry: [sendParent('ONLINE')] | |
}, | |
offline: { | |
on: { | |
ONLINE: 'online' | |
}, | |
after: { | |
ONLINE_CHECK_INTERVAL: 'checkingConnectivity' | |
} | |
} | |
} | |
} | |
} | |
}, | |
{ | |
delays: { | |
ONLINE_CHECK_INTERVAL: ctx => ctx.onlineCheckInterval, | |
}, | |
services: { | |
checkRemote: ctx => window.fetch(ctx.remoteOnlineCheckURL), | |
offlineListener: ctx => callback => { | |
const sendOnline = () => { | |
callback('ONLINE') | |
sendParent('ONLINE') | |
} | |
const sendOffline = () => { | |
callback('CHECK_REMOTE') | |
} | |
window.addEventListener('online', sendOnline) | |
window.addEventListener('offline', sendOffline) | |
return () => { | |
window.removeEventListener('online', sendOnline) | |
window.removeEventListener('offline', sendOffline) | |
} | |
} | |
} | |
} | |
) | |
const realtime = Machine( | |
{ | |
id: 'realtime', | |
initial: 'init', | |
context: { | |
realtimeRetryInterval: 25 * 1000, | |
}, | |
states: { | |
init: { | |
on: { | |
'': 'connecting' | |
} | |
}, | |
connecting: { | |
invoke: { | |
src: 'startLongpolling', | |
onDone: 'connected', | |
onError: 'disconnected' | |
} | |
}, | |
connected: { | |
invoke: { | |
src: 'realtimeDataParser' | |
}, | |
on: { | |
REALTIME_NETWORK_ERROR: 'disconnected', | |
REALTIME_DISCONNECT: 'disconnected' | |
} | |
}, | |
disconnected: { | |
after: { | |
REALTIME_RETRY_INTERVAL: 'connecting' | |
} | |
} | |
} | |
}, | |
{ | |
delays: { | |
REALTIME_RETRY_INTERVAL: ctx => ctx.realtimeRetryInterval | |
}, | |
services: { | |
startLongpolling: ctx => Promise.resolve(), | |
realtimeDataParser: ctx => callback => { | |
// TODO: get XHR from ctx | |
// callback('REALTIME_TIEMOUT'), | |
// callback('REALTIME_DISCONNECT') | |
// callback('REALTIME_PARSED_DATA') | |
}, | |
} | |
} | |
) | |
const scheduleRequest = request => ctx => { | |
return [...ctx.networkQueue, Promise.resolve(request)] | |
} | |
const pullStep = (entity, nextEntity, parallel) => { | |
return { | |
on: { | |
PULL_NEXT: nextEntity | |
}, | |
initial: 'pulling', | |
states: { | |
pulling: { | |
entry: [ | |
assign({ | |
networkQueue: scheduleRequest(entity) | |
}), | |
parallel ? send('PULL_NEXT') : send('FETCH_ENTITIES') | |
] | |
} | |
} | |
} | |
} | |
const networkStates = { | |
initial: 'ready', | |
states: { | |
ready: { | |
on: { | |
FETCH_ENTITIES: 'fetching' | |
} | |
}, | |
fetching: { | |
invoke: { | |
src: 'pullRemoteData', | |
onDone: { | |
target: 'ready', | |
actions: [ | |
assign({ | |
networkQueue: ctx => [], | |
pullResults: (ctx, event) => { | |
return [...ctx.pullResults, event.data] | |
} | |
}), | |
send('PULL_NEXT') | |
] | |
}, | |
onError: { | |
target: 'ready', | |
actions: [sendParent('SYNC_FAILURE')] | |
} | |
} | |
} | |
} | |
} | |
const pullingOrderStates = { | |
initial: 'settings', | |
states: { | |
settings: pullStep('settings', 'capabilities', true), | |
capabilities: pullStep('capabilities', 'listGroups'), | |
listGroups: pullStep('listGroups', 'lists'), | |
lists: pullStep('lists', 'tasks'), | |
tasks: pullStep('tasks', 'members', true), | |
members: pullStep('members', 'taskSuggestions'), | |
taskSuggestions: pullStep('taskSuggestions', 'finished'), | |
finished: { | |
entry: [send('DONE')], | |
type: 'final', | |
} | |
} | |
} | |
const pullingQueue = Machine( | |
{ | |
id: 'pullingQueue', | |
initial: 'working', | |
context: { | |
networkQueue: [], | |
pullResults: [] | |
}, | |
states: { | |
working: { | |
on: { | |
DONE: 'done' | |
}, | |
type: 'parallel', | |
states: { | |
pullingOrder: pullingOrderStates, | |
network: networkStates | |
} | |
}, | |
done: { | |
type: 'final', | |
data: { | |
results: ctx => ctx.pullResults | |
} | |
} | |
} | |
}, | |
{ | |
services: { | |
pullRemoteData: ctx => { | |
const tasks = ctx.networkQueue | |
return Promise.all(tasks) | |
} | |
} | |
} | |
) | |
const pushQueue = Machine( | |
{ | |
id: 'pushQueue', | |
initial: 'working', | |
context: { | |
changes: [] | |
}, | |
states: { | |
working: { | |
invoke: { | |
src: 'pushingService', | |
onDone: 'cleared', | |
onError: 'failed' | |
}, | |
on: { | |
'': { | |
target: 'cleared', | |
cond: 'isQueueEmpty' | |
} | |
} | |
}, | |
failed: { | |
type: 'final', | |
entry: [send('SYNC_FAILURE'), sendParent('SYNC_FAILURE')], | |
on: { | |
'': 'cleared' | |
}, | |
data: { | |
remainingChanges: ctx => ctx.changes | |
} | |
}, | |
cleared: { | |
type: 'final', | |
data: { | |
remainingChanges: ctx => ctx.changes | |
} | |
} | |
} | |
}, | |
{ | |
guards: { | |
isQueueEmpty: ctx => ctx.changes.length === 0 | |
}, | |
services: { | |
pushingService: ctx => { | |
// TODO | |
return Promise.resolve() | |
}, | |
} | |
} | |
) | |
const pushPullSyncStates = { | |
initial: 'started', | |
states: { | |
started: { | |
initial: 'pushing', | |
on: { | |
SYNC_SUCCESS: 'synced', | |
SYNC_FAILURE: 'errored' | |
// OFFLINE: '#app.sync.stopped.offline' | |
}, | |
states: { | |
pushing: { | |
invoke: { | |
src: 'pushQueue', | |
onDone: { | |
target: 'pulling', | |
actions: [ | |
assign({ | |
queueState: (ctx, event) => event.data.remainingChanges | |
}) | |
] | |
} | |
} | |
}, | |
pulling: { | |
invoke: { | |
src: 'pullingQueue', | |
onDone: { | |
actions: [ | |
assign({ | |
pullResults: (ctx, event) => event.data.results | |
}), | |
send('SYNC_SUCCESS') | |
] | |
} | |
} | |
} | |
} | |
}, | |
synced: { | |
on: { | |
'': 'periodicSync' | |
} | |
}, | |
errored: { | |
entry: 'saveError', | |
on: { | |
'': [ | |
{ target: '#app.sync.blocked', cond: 'shouldBlockSync' }, | |
{ target: 'backoff' } | |
] | |
} | |
}, | |
backoff: { | |
on: { | |
// OFFLINE: '#app.sync.stopped.offline' | |
}, | |
after: { | |
BACKOFF_INTERVAL: 'started' | |
}, | |
exit: 'increaseBackoff' | |
}, | |
periodicSync: { | |
on: { | |
// OFFLINE: '#app.sync.stopped.offline' | |
}, | |
after: { | |
PERIODIC_SYNC_INTERVAL: 'started' | |
} | |
} | |
} | |
} | |
const syncStates = { | |
initial: 'stopped', | |
states: { | |
syncing: { | |
initial: 'online', | |
on: { | |
STOP_SYNC: 'stopped' | |
}, | |
states: { | |
online: { | |
on: { | |
'': { | |
target: '#app.sync.stopped', | |
cond: 'isOffline' | |
}, | |
OFFLINE: { | |
target: '#app.sync.stopped.offline' | |
} | |
}, | |
type: 'parallel', | |
states: { | |
pushPull: pushPullSyncStates, | |
realtime: { | |
invoke: { | |
src: 'realtime' | |
} | |
} | |
} | |
} | |
} | |
}, | |
stopped: { | |
initial: 'init', | |
on: { | |
START_SYNC: 'syncing' | |
}, | |
states: { | |
init: { | |
on: { | |
'': { | |
target: 'offline', | |
cond: 'isOffline' | |
} | |
} | |
}, | |
offline: { | |
on: { | |
ONLINE: '#app.sync.syncing' | |
}, | |
after: { | |
60000: '#app.sync.syncing' | |
} | |
} | |
} | |
}, | |
blocked: { | |
type: 'final' | |
} | |
} | |
} | |
const sync = Machine( | |
{ | |
id: 'app', | |
// the initial context (extended state) of the statechart | |
context: { | |
syncToken: null, //prompt('Sync Token'), | |
sessionId: 'x-state-testing', | |
correlationVector: 'x-state-testing', | |
features: {}, | |
backoffInterval: 4 * 1000, | |
periodicSyncInterval: 120 * 1000, | |
lastHTTPError: null, | |
queueState: [], | |
pullResults: [] | |
}, | |
type: 'parallel', | |
states: { | |
sync: syncStates, | |
onlineCheck: { | |
invoke: { | |
src: 'onlineChecker' | |
} | |
} | |
} | |
}, | |
{ | |
actions: { | |
saveError: assign({ | |
lastHTTPError: (ctx, event) => event.data && event.data.error | |
}), | |
increaseBackoff: assign({ | |
backoffInterval: ctx => { | |
if (ctx.lastHTTPError && ctx.lastHTTPError.httpCode == 429) { | |
return Math.max(ctx.backoffInterval, 300 * 1000) | |
} else { | |
return Math.min(ctx.backoffInterval * 2, 512 * 1000) | |
} | |
} | |
}) | |
}, | |
delays: { | |
PERIODIC_SYNC_INTERVAL: ctx => ctx.periodicSyncInterval, | |
BACKOFF_INTERVAL: ctx => ctx.backoffInterval, | |
}, | |
guards: { | |
isOffline: ctx => !navigator.onLine, | |
shouldBlockSync: ctx => | |
[ | |
'DomainNotFound', | |
'MailboxNotEnabledForRESTAPI', | |
'MailboxNotSupportedForRESTAPI', | |
'RESTAPINotEnabledForComponentSharedMailbox' | |
].includes(ctx.lastHTTPError && ctx.lastHTTPError.errorCode), | |
}, | |
services: { | |
pushQueue, | |
pullingQueue, | |
realtime, | |
onlineChecker | |
} | |
} | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment