UNPKG

@xstate/inspect

Version:
276 lines (273 loc) 10.8 kB
import { __assign } from './_virtual/_tslib.js'; import { interpret, toSCXMLEvent, toEventObject, toObserver, toActorRef } from 'xstate'; import { createInspectMachine } from './inspectMachine.js'; import { stringifyMachine, stringifyState } from './serialize.js'; import { getLazy, stringify, isReceiverEvent, parseReceiverEvent } from './utils.js'; var serviceMap = new Map(); function createDevTools() { var services = new Set(); var serviceListeners = new Set(); return { services: services, register: function (service) { services.add(service); serviceMap.set(service.sessionId, service); serviceListeners.forEach(function (listener) { return listener(service); }); service.onStop(function () { services.delete(service); serviceMap.delete(service.sessionId); }); }, unregister: function (service) { services.delete(service); serviceMap.delete(service.sessionId); }, onRegister: function (listener) { serviceListeners.add(listener); services.forEach(function (service) { return listener(service); }); return { unsubscribe: function () { serviceListeners.delete(listener); } }; } }; } var defaultInspectorOptions = { url: 'https://stately.ai/viz?inspect', iframe: function () { return document.querySelector('iframe[data-xstate]'); }, devTools: function () { var devTools = createDevTools(); globalThis.__xstate__ = devTools; return devTools; }, serialize: undefined, targetWindow: undefined }; var getFinalOptions = function (options) { var withDefaults = __assign(__assign({}, defaultInspectorOptions), options); return __assign(__assign({}, withDefaults), { url: new URL(withDefaults.url), iframe: getLazy(withDefaults.iframe), devTools: getLazy(withDefaults.devTools) }); }; var patchedInterpreters = new Set(); function inspect(options) { var finalOptions = getFinalOptions(options); var iframe = finalOptions.iframe, url = finalOptions.url, devTools = finalOptions.devTools; if ((options === null || options === void 0 ? void 0 : options.targetWindow) === null) { throw new Error('Received a nullable `targetWindow`.'); } var targetWindow = finalOptions.targetWindow; if (iframe === null && !targetWindow) { console.warn('No suitable <iframe> found to embed the inspector. Please pass an <iframe> element to `inspect(iframe)` or create an <iframe data-xstate></iframe> element.'); return undefined; } var inspectMachine = createInspectMachine(devTools, options); var inspectService = interpret(inspectMachine).start(); var listeners = new Set(); var sub = inspectService.subscribe(function (state) { listeners.forEach(function (listener) { return listener.next(state); }); }); var client; var messageHandler = function (event) { if (typeof event.data === 'object' && event.data !== null && 'type' in event.data) { if (iframe && !targetWindow) { targetWindow = iframe.contentWindow; } if (!client) { client = { send: function (e) { targetWindow.postMessage(e, url.origin); } }; } var inspectEvent = __assign(__assign({}, event.data), { client: client }); inspectService.send(inspectEvent); } }; window.addEventListener('message', messageHandler); window.addEventListener('unload', function () { inspectService.send({ type: 'unload' }); }); var stringifyWithSerializer = function (value) { return stringify(value, options === null || options === void 0 ? void 0 : options.serialize); }; devTools.onRegister(function (service) { var _a; var state = service.state || service.initialState; inspectService.send({ type: 'service.register', machine: stringifyMachine(service.machine, options === null || options === void 0 ? void 0 : options.serialize), state: stringifyState(state, options === null || options === void 0 ? void 0 : options.serialize), sessionId: service.sessionId, id: service.id, parent: (_a = service.parent) === null || _a === void 0 ? void 0 : _a.sessionId }); inspectService.send({ type: 'service.event', event: stringifyWithSerializer(state._event), sessionId: service.sessionId }); if (!patchedInterpreters.has(service)) { patchedInterpreters.add(service); // monkey-patch service.send so that we know when an event was sent // to a service *before* it is processed, since other events might occur // while the sent one is being processed, which throws the order off var originalSend_1 = service.send.bind(service); service.send = function inspectSend(event, payload) { inspectService.send({ type: 'service.event', event: stringifyWithSerializer(toSCXMLEvent(toEventObject(event, payload))), sessionId: service.sessionId }); return originalSend_1(event, payload); }; } service.subscribe(function (state) { // filter out synchronous notification from within `.start()` call // when the `service.state` has not yet been assigned if (state === undefined) { return; } inspectService.send({ type: 'service.state', // TODO: investigate usage of structuredClone in browsers if available state: stringifyState(state, options === null || options === void 0 ? void 0 : options.serialize), sessionId: service.sessionId }); }); service.onStop(function () { inspectService.send({ type: 'service.stop', sessionId: service.sessionId }); }); }); if (iframe) { iframe.addEventListener('load', function () { targetWindow = iframe.contentWindow; }); iframe.setAttribute('src', String(url)); } else if (!targetWindow) { targetWindow = window.open(String(url), 'xstateinspector'); } return { send: function (event) { inspectService.send(event); }, subscribe: function (next, onError, onComplete) { var observer = toObserver(next, onError, onComplete); listeners.add(observer); observer.next(inspectService.state); return { unsubscribe: function () { listeners.delete(observer); } }; }, disconnect: function () { inspectService.send('disconnect'); window.removeEventListener('message', messageHandler); sub.unsubscribe(); } }; } function createWindowReceiver(options) { var _a = options || {}, _b = _a.window, ownWindow = _b === void 0 ? window : _b, _c = _a.targetWindow, targetWindow = _c === void 0 ? window.self === window.top ? window.opener : window.parent : _c; var observers = new Set(); var latestEvent; var handler = function (event) { var data = event.data; if (isReceiverEvent(data)) { latestEvent = parseReceiverEvent(data); observers.forEach(function (listener) { return listener.next(latestEvent); }); } }; ownWindow.addEventListener('message', handler); var actorRef = toActorRef({ id: 'xstate.windowReceiver', send: function (event) { if (!targetWindow) { return; } targetWindow.postMessage(event, '*'); }, subscribe: function (next, onError, onComplete) { var observer = toObserver(next, onError, onComplete); observers.add(observer); return { unsubscribe: function () { observers.delete(observer); } }; }, stop: function () { observers.clear(); ownWindow.removeEventListener('message', handler); }, getSnapshot: function () { return latestEvent; } }); actorRef.send({ type: 'xstate.inspecting' }); return actorRef; } function createWebSocketReceiver(options) { var _a = options.protocol, protocol = _a === void 0 ? 'ws' : _a; var ws = new WebSocket("".concat(protocol, "://").concat(options.server)); var observers = new Set(); var latestEvent; var actorRef = toActorRef({ id: 'xstate.webSocketReceiver', send: function (event) { ws.send(stringify(event, options.serialize)); }, subscribe: function (next, onError, onComplete) { var observer = toObserver(next, onError, onComplete); observers.add(observer); return { unsubscribe: function () { observers.delete(observer); } }; }, getSnapshot: function () { return latestEvent; } }); ws.onopen = function () { actorRef.send({ type: 'xstate.inspecting' }); }; ws.onmessage = function (event) { if (typeof event.data !== 'string') { return; } try { var eventObject = JSON.parse(event.data); if (isReceiverEvent(eventObject)) { latestEvent = parseReceiverEvent(eventObject); observers.forEach(function (observer) { observer.next(latestEvent); }); } } catch (e) { console.error(e); } }; ws.onerror = function (err) { observers.forEach(function (observer) { var _a; (_a = observer.error) === null || _a === void 0 ? void 0 : _a.call(observer, err); }); }; return actorRef; } export { createDevTools, createWebSocketReceiver, createWindowReceiver, inspect, serviceMap };