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 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
| // 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