Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@xstate/inspect

Package Overview
Dependencies
Maintainers
1
Versions
29
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@xstate/inspect - npm Package Compare versions

Comparing version 0.3.0 to 0.4.0

es/browser.d.ts

63

CHANGELOG.md
# @xstate/inspect
## 0.4.0
### Minor Changes
- [`63ba888e`](https://github.com/davidkpiano/xstate/commit/63ba888e19bd2b72f9aad2c9cd36cde297e0ffe5) [#1770](https://github.com/davidkpiano/xstate/pull/1770) Thanks [@davidkpiano](https://github.com/davidkpiano)! - It is now easier for developers to create their own XState inspectors, and even inspect services offline.
A **receiver** is an actor that receives inspector events from a source, such as `"service.register"`, `"service.state"`, `"service.event"`, etc. This update includes two receivers:
- `createWindowReceiver` - listens to inspector events from a parent window (for both popup and iframe scenarios)
- 🚧 `createWebSocketReceiver` (experimental) - listens to inspector events from a WebSocket server
Here's how it works:
**Application (browser) code**
```js
import { inspect } from '@xstate/inspect';
inspect(/* options */);
// ...
interpret(someMachine, { devTools: true }).start();
```
**Inspector code**
```js
import { createWindowReceiver } from '@xstate/inspect';
const windowReceiver = createWindowReceiver(/* options? */);
windowReceiver.subscribe(event => {
// here, you will receive events like:
// { type: "service.register", machine: ..., state: ..., sessionId: ... }
console.log(event);
});
```
The events you will receive are `ParsedReceiverEvent` types:
```ts
export type ParsedReceiverEvent =
| {
type: 'service.register';
machine: StateMachine<any, any, any>;
state: State<any, any>;
id: string;
sessionId: string;
parent?: string;
source?: string;
}
| { type: 'service.stop'; sessionId: string }
| {
type: 'service.state';
state: State<any, any>;
sessionId: string;
}
| { type: 'service.event'; event: SCXML.Event<any>; sessionId: string };
```
Given these events, you can visualize the service machines and their states and events however you'd like.
## 0.3.0

@@ -4,0 +67,0 @@

20

es/_virtual/_tslib.js
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
Copyright (c) Microsoft Corporation.
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */

@@ -15,0 +15,0 @@

@@ -1,27 +0,3 @@

import { Interpreter, EventObject } from 'xstate';
import { XStateDevInterface } from 'xstate/lib/devTools';
export declare type ServiceListener = (service: Interpreter<any>) => void;
declare type MaybeLazy<T> = T | (() => T);
export interface InspectorOptions {
url: string;
iframe: MaybeLazy<HTMLIFrameElement | null | false>;
devTools: MaybeLazy<XStateDevInterface>;
}
export declare function createDevTools(): XStateDevInterface;
export declare const createInspectMachine: (devTools?: XStateDevInterface) => import("xstate").StateMachine<{
client?: any;
}, any, import("xstate").AnyEventObject, {
value: any;
context: {
client?: any;
};
}>;
export declare function inspect(options?: Partial<InspectorOptions>): {
send: (event: EventObject) => void;
/**
* Disconnects the inspector.
*/
disconnect: () => void;
};
export {};
export { inspect, createWindowReceiver, createWebSocketReceiver, createDevTools } from './browser';
export * from './types';
//# sourceMappingURL=index.d.ts.map

@@ -1,216 +0,1 @@

import { __assign } from './_virtual/_tslib.js';
import { createMachine, assign, interpret } from 'xstate';
import { toSCXMLEvent, toEventObject } from 'xstate/lib/utils';
import safeStringify from 'fast-safe-stringify';
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 createInspectMachine = function (devTools) {
if (devTools === void 0) { devTools = globalThis.__xstate__; }
return createMachine({
initial: 'pendingConnection',
context: {
client: undefined
},
states: {
pendingConnection: {},
connected: {
on: {
'service.state': {
actions: function (ctx, e) { return ctx.client.send(e); }
},
'service.event': {
actions: function (ctx, e) { return ctx.client.send(e); }
},
'service.register': {
actions: function (ctx, e) { return ctx.client.send(e); }
},
'service.stop': {
actions: function (ctx, e) { return ctx.client.send(e); }
},
'xstate.event': {
actions: function (_, e) {
var event = e.event;
var scxmlEventObject = JSON.parse(event);
var service = serviceMap.get(scxmlEventObject.origin);
service === null || service === void 0 ? void 0 : service.send(scxmlEventObject);
}
},
unload: {
actions: function (ctx) {
ctx.client.send({ type: 'xstate.disconnect' });
}
},
disconnect: 'disconnected'
}
},
disconnected: {
type: 'final'
}
},
on: {
'xstate.inspecting': {
target: '.connected',
actions: [
assign({ client: function (_, e) { return e.client; } }),
function (ctx) {
devTools.services.forEach(function (service) {
ctx.client.send({
type: 'service.register',
machine: stringify(service.machine),
state: stringify(service.state || service.initialState),
sessionId: service.sessionId
});
});
}
]
}
}
});
};
var defaultInspectorOptions = {
url: 'https://statecharts.io/inspect',
iframe: function () {
return document.querySelector('iframe[data-xstate]');
},
devTools: function () {
var devTools = createDevTools();
globalThis.__xstate__ = devTools;
return devTools;
}
};
function getLazy(value) {
return typeof value === 'function' ? value() : value;
}
function stringify(value) {
try {
return JSON.stringify(value);
}
catch (e) {
return safeStringify(value);
}
}
function inspect(options) {
var _a = __assign(__assign({}, defaultInspectorOptions), options), iframe = _a.iframe, url = _a.url, devTools = _a.devTools;
var resolvedDevTools = getLazy(devTools);
var resolvedIframe = getLazy(iframe);
var inspectMachine = createInspectMachine(resolvedDevTools);
var targetWindow;
var inspectService = interpret(inspectMachine).start();
if (resolvedIframe === null) {
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.');
inspectService.send('disconnect');
return { send: function () { return void 0; }, disconnect: function () { return void 0; } };
}
var client;
var messageHandler = function (event) {
if (typeof event.data === 'object' &&
event.data !== null &&
'type' in event.data) {
if (resolvedIframe && !targetWindow) {
targetWindow = resolvedIframe.contentWindow;
}
if (!client) {
client = {
send: function (e) {
targetWindow.postMessage(e, url);
}
};
}
inspectService.send(__assign(__assign({}, event.data), { client: client }));
}
};
window.addEventListener('message', messageHandler);
window.addEventListener('unload', function () {
inspectService.send({ type: 'unload' });
});
if (resolvedIframe === false) {
targetWindow = window.open(url, 'xstateinspector');
}
resolvedDevTools.onRegister(function (service) {
var _a;
inspectService.send({
type: 'service.register',
machine: stringify(service.machine),
state: stringify(service.state || service.initialState),
sessionId: service.sessionId,
id: service.id,
parent: (_a = service.parent) === null || _a === void 0 ? void 0 : _a.sessionId
});
inspectService.send({
type: 'service.event',
event: stringify((service.state || service.initialState)._event),
sessionId: service.sessionId
});
// 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 = service.send.bind(service);
service.send = function inspectSend(event, payload) {
inspectService.send({
type: 'service.event',
event: stringify(toSCXMLEvent(toEventObject(event, payload))),
sessionId: service.sessionId
});
return originalSend(event, payload);
};
service.subscribe(function (state) {
inspectService.send({
type: 'service.state',
state: stringify(state),
sessionId: service.sessionId
});
});
service.onStop(function () {
inspectService.send({
type: 'service.stop',
sessionId: service.sessionId
});
});
});
if (resolvedIframe) {
resolvedIframe.addEventListener('load', function () {
targetWindow = resolvedIframe.contentWindow;
});
resolvedIframe.setAttribute('src', url);
}
return {
send: function (event) {
inspectService.send(event);
},
disconnect: function () {
inspectService.send('disconnect');
window.removeEventListener('message', messageHandler);
}
};
}
export { createDevTools, createInspectMachine, inspect };
export { createDevTools, createWebSocketReceiver, createWindowReceiver, inspect } from './browser.js';

@@ -1,2 +0,8 @@

export declare function inspectServer(): void;
import * as WebSocket from 'ws';
import { Inspector } from './types';
interface ServerInspectorOptions {
server: WebSocket.Server;
}
export declare function inspect(options: ServerInspectorOptions): Inspector;
export {};
//# sourceMappingURL=server.d.ts.map

@@ -6,14 +6,14 @@ 'use strict';

/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
Copyright (c) Microsoft Corporation.
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */

@@ -20,0 +20,0 @@

@@ -1,27 +0,3 @@

import { Interpreter, EventObject } from 'xstate';
import { XStateDevInterface } from 'xstate/lib/devTools';
export declare type ServiceListener = (service: Interpreter<any>) => void;
declare type MaybeLazy<T> = T | (() => T);
export interface InspectorOptions {
url: string;
iframe: MaybeLazy<HTMLIFrameElement | null | false>;
devTools: MaybeLazy<XStateDevInterface>;
}
export declare function createDevTools(): XStateDevInterface;
export declare const createInspectMachine: (devTools?: XStateDevInterface) => import("xstate").StateMachine<{
client?: any;
}, any, import("xstate").AnyEventObject, {
value: any;
context: {
client?: any;
};
}>;
export declare function inspect(options?: Partial<InspectorOptions>): {
send: (event: EventObject) => void;
/**
* Disconnects the inspector.
*/
disconnect: () => void;
};
export {};
export { inspect, createWindowReceiver, createWebSocketReceiver, createDevTools } from './browser';
export * from './types';
//# sourceMappingURL=index.d.ts.map

@@ -5,221 +5,9 @@ 'use strict';

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var browser = require('./browser.js');
var _tslib = require('./_virtual/_tslib.js');
var xstate = require('xstate');
var utils = require('xstate/lib/utils');
var safeStringify = _interopDefault(require('fast-safe-stringify'));
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 createInspectMachine = function (devTools) {
if (devTools === void 0) { devTools = globalThis.__xstate__; }
return xstate.createMachine({
initial: 'pendingConnection',
context: {
client: undefined
},
states: {
pendingConnection: {},
connected: {
on: {
'service.state': {
actions: function (ctx, e) { return ctx.client.send(e); }
},
'service.event': {
actions: function (ctx, e) { return ctx.client.send(e); }
},
'service.register': {
actions: function (ctx, e) { return ctx.client.send(e); }
},
'service.stop': {
actions: function (ctx, e) { return ctx.client.send(e); }
},
'xstate.event': {
actions: function (_, e) {
var event = e.event;
var scxmlEventObject = JSON.parse(event);
var service = serviceMap.get(scxmlEventObject.origin);
service === null || service === void 0 ? void 0 : service.send(scxmlEventObject);
}
},
unload: {
actions: function (ctx) {
ctx.client.send({ type: 'xstate.disconnect' });
}
},
disconnect: 'disconnected'
}
},
disconnected: {
type: 'final'
}
},
on: {
'xstate.inspecting': {
target: '.connected',
actions: [
xstate.assign({ client: function (_, e) { return e.client; } }),
function (ctx) {
devTools.services.forEach(function (service) {
ctx.client.send({
type: 'service.register',
machine: stringify(service.machine),
state: stringify(service.state || service.initialState),
sessionId: service.sessionId
});
});
}
]
}
}
});
};
var defaultInspectorOptions = {
url: 'https://statecharts.io/inspect',
iframe: function () {
return document.querySelector('iframe[data-xstate]');
},
devTools: function () {
var devTools = createDevTools();
globalThis.__xstate__ = devTools;
return devTools;
}
};
function getLazy(value) {
return typeof value === 'function' ? value() : value;
}
function stringify(value) {
try {
return JSON.stringify(value);
}
catch (e) {
return safeStringify(value);
}
}
function inspect(options) {
var _a = _tslib.__assign(_tslib.__assign({}, defaultInspectorOptions), options), iframe = _a.iframe, url = _a.url, devTools = _a.devTools;
var resolvedDevTools = getLazy(devTools);
var resolvedIframe = getLazy(iframe);
var inspectMachine = createInspectMachine(resolvedDevTools);
var targetWindow;
var inspectService = xstate.interpret(inspectMachine).start();
if (resolvedIframe === null) {
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.');
inspectService.send('disconnect');
return { send: function () { return void 0; }, disconnect: function () { return void 0; } };
}
var client;
var messageHandler = function (event) {
if (typeof event.data === 'object' &&
event.data !== null &&
'type' in event.data) {
if (resolvedIframe && !targetWindow) {
targetWindow = resolvedIframe.contentWindow;
}
if (!client) {
client = {
send: function (e) {
targetWindow.postMessage(e, url);
}
};
}
inspectService.send(_tslib.__assign(_tslib.__assign({}, event.data), { client: client }));
}
};
window.addEventListener('message', messageHandler);
window.addEventListener('unload', function () {
inspectService.send({ type: 'unload' });
});
if (resolvedIframe === false) {
targetWindow = window.open(url, 'xstateinspector');
}
resolvedDevTools.onRegister(function (service) {
var _a;
inspectService.send({
type: 'service.register',
machine: stringify(service.machine),
state: stringify(service.state || service.initialState),
sessionId: service.sessionId,
id: service.id,
parent: (_a = service.parent) === null || _a === void 0 ? void 0 : _a.sessionId
});
inspectService.send({
type: 'service.event',
event: stringify((service.state || service.initialState)._event),
sessionId: service.sessionId
});
// 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 = service.send.bind(service);
service.send = function inspectSend(event, payload) {
inspectService.send({
type: 'service.event',
event: stringify(utils.toSCXMLEvent(utils.toEventObject(event, payload))),
sessionId: service.sessionId
});
return originalSend(event, payload);
};
service.subscribe(function (state) {
inspectService.send({
type: 'service.state',
state: stringify(state),
sessionId: service.sessionId
});
});
service.onStop(function () {
inspectService.send({
type: 'service.stop',
sessionId: service.sessionId
});
});
});
if (resolvedIframe) {
resolvedIframe.addEventListener('load', function () {
targetWindow = resolvedIframe.contentWindow;
});
resolvedIframe.setAttribute('src', url);
}
return {
send: function (event) {
inspectService.send(event);
},
disconnect: function () {
inspectService.send('disconnect');
window.removeEventListener('message', messageHandler);
}
};
}
exports.createDevTools = createDevTools;
exports.createInspectMachine = createInspectMachine;
exports.inspect = inspect;
exports.createDevTools = browser.createDevTools;
exports.createWebSocketReceiver = browser.createWebSocketReceiver;
exports.createWindowReceiver = browser.createWindowReceiver;
exports.inspect = browser.inspect;

@@ -1,2 +0,8 @@

export declare function inspectServer(): void;
import * as WebSocket from 'ws';
import { Inspector } from './types';
interface ServerInspectorOptions {
server: WebSocket.Server;
}
export declare function inspect(options: ServerInspectorOptions): Inspector;
export {};
//# sourceMappingURL=server.d.ts.map
{
"name": "@xstate/inspect",
"version": "0.3.0",
"version": "0.4.0",
"description": "XState inspection utilities",

@@ -42,4 +42,4 @@ "keywords": [

"@types/ws": "^7.2.6",
"rollup": "^1.26.3",
"rollup-plugin-typescript2": "^0.25.2",
"rollup": "^2.35.1",
"rollup-plugin-typescript2": "^0.29.0",
"typescript": "^4.1.2",

@@ -46,0 +46,0 @@ "ws": "^7.3.1",

@@ -86,2 +86,86 @@ # `@xstate/inspect`

## Implementing
You can implement your own inspector by creating a **receiver**. A **receiver** is an actor that receives inspector events from a source (like a parent window or a WebSocket connection):
- `"service.register"`
```ts
{
type: 'service.register';
machine: StateMachine<any, any, any>;
state: State<any, any>;
id: string;
sessionId: string;
parent?: string;
source?: string;
}
```
- `"service.stop"`
```ts
{
type: 'service.stop';
sessionId: string;
}
```
- `"service.state"`
```ts
{
type: 'service.state';
state: State<any, any>;
sessionId: string;
}
```
- `"service.event"`
```ts
{
type: 'service.event';
event: SCXML.Event<any>;
sessionId: string
};
```
To listen to events from an inspected source, create a receiver with the appropriate `create*Receiver(...)` function; for example:
```js
import { createWindowReceiver } from '@xstate/inspect';
const windowReceiver = createWindowReceiver(/* options? */);
windowReceiver.subscribe((event) => {
// here, you will receive "service.*" events
console.log(event);
});
```
You can also send events to the receiver:
```js
// ...
// This will send the event to the inspected service
windowReceiver.send({
type: 'xstate.event',
event: JSON.stringify({ type: 'someEvent' }),
service: /* session ID of the service this event is sent to */
});
```
The typical inspection workflow is as follows:
1. The `inspect(/* ... */)` call on the client opens the inspector (e.g., in a separate window, or creates a WebSocket connection)
2. The receiver sends an `"xstate.inspecting"` event to the client
3. The client sends `"service.register"` events to the receiver
4. An inspector listening to the receiver (via `receiver.subscribe(...)`) registers the machine (`event.machine`) by its `event.sessionId`
5. The machine is visually rendered, and its current state (`event.state`) is highlighted
6. As the service at the source receives events and changes state, it will send the receiver `"service.event"` and `"service.state"` events, respectively
7. The inspector can use those events to highlight the current state and keep a log of events sent to that service
8. When the service stops, a `"service.stop"` event is sent to the receiver with the `event.sessionId` to identify the stopped service.
## FAQs

@@ -88,0 +172,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc