@xstate/inspect
Version:
XState inspection utilities
276 lines (273 loc) • 10.8 kB
JavaScript
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 };