Skip to content

Instantly share code, notes, and snippets.

@mr-mig
Last active November 22, 2019 13:20
Show Gist options
  • Save mr-mig/57d0f3da972f353fa57b488fdc6f3464 to your computer and use it in GitHub Desktop.
Save mr-mig/57d0f3da972f353fa57b488fdc6f3464 to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
// 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