From c8787338e100f45649b14eae49f3eddacefd7df9 Mon Sep 17 00:00:00 2001 From: Anders Bech Mellson Date: Fri, 26 Jan 2024 12:58:15 +0100 Subject: [PATCH 1/5] createSkyInspector --- .changeset/friendly-pianos-itch.md | 5 +++ package.json | 9 ++++- pnpm-lock.yaml | 63 +++++++++++++++++++++++++++--- scripts/dev.sh | 21 ++++++++++ src/createSkyInspector.ts | 58 +++++++++++++++++++++++++++ src/index.ts | 7 ++-- src/utils.ts | 7 +++- 7 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 .changeset/friendly-pianos-itch.md create mode 100755 scripts/dev.sh create mode 100644 src/createSkyInspector.ts diff --git a/.changeset/friendly-pianos-itch.md b/.changeset/friendly-pianos-itch.md new file mode 100644 index 0000000..fe0bd08 --- /dev/null +++ b/.changeset/friendly-pianos-itch.md @@ -0,0 +1,5 @@ +--- +'@statelyai/inspect': patch +--- + +Adds `createSkyInspector`, which allows you to inspect machines in Node or the browser. The inspection will send the events to a server backend through websockets and allows you to open and share a live inspection URL. diff --git a/package.json b/package.json index f3c2cd7..01ba9f4 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.26.2", "@types/jsdom": "^21.1.6", + "@types/uuid": "^9.0.8", "jsdom": "^23.0.0", "tsup": "^7.2.0", "typescript": "^5.1.6", @@ -18,7 +19,10 @@ "license": "MIT", "dependencies": { "fast-safe-stringify": "^2.1.1", - "isomorphic-ws": "^5.0.0" + "isomorphic-ws": "^5.0.0", + "partysocket": "^0.0.25", + "superjson": "^1", + "uuid": "^9.0.1" }, "peerDependencies": { "xstate": "^5.5.1" @@ -30,7 +34,8 @@ "prepublishOnly": "tsup src/index.ts --dts", "changeset": "changeset", "release": "changeset publish", - "version": "changeset version" + "version": "changeset version", + "dev": "yarn build && ./scripts/dev.sh" }, "publishConfig": { "access": "public" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ebc081..6ae1e3b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,16 @@ dependencies: version: 2.1.1 isomorphic-ws: specifier: ^5.0.0 - version: 5.0.0(ws@8.14.2) + version: 5.0.0(ws@8.16.0) + partysocket: + specifier: ^0.0.25 + version: 0.0.25 + superjson: + specifier: ^1 + version: 1.13.3 + uuid: + specifier: ^9.0.1 + version: 9.0.1 devDependencies: '@changesets/changelog-github': @@ -22,6 +31,9 @@ devDependencies: '@types/jsdom': specifier: ^21.1.6 version: 21.1.6 + '@types/uuid': + specifier: ^9.0.8 + version: 9.0.8 jsdom: specifier: ^23.0.0 version: 23.0.0 @@ -606,6 +618,10 @@ packages: resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} dev: true + /@types/uuid@9.0.8: + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + dev: true + /@vitest/expect@0.34.6: resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} dependencies: @@ -956,6 +972,13 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + dependencies: + is-what: 4.1.16 + dev: false + /cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} dependencies: @@ -1239,6 +1262,11 @@ packages: hasBin: true dev: true + /event-target-shim@6.0.2: + resolution: {integrity: sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==} + engines: {node: '>=10.13.0'} + dev: false + /execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -1750,6 +1778,11 @@ packages: call-bind: 1.0.5 dev: true + /is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + dev: false + /is-windows@1.0.2: resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} engines: {node: '>=0.10.0'} @@ -1763,12 +1796,12 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /isomorphic-ws@5.0.0(ws@8.14.2): + /isomorphic-ws@5.0.0(ws@8.16.0): resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: ws: '*' dependencies: - ws: 8.14.2 + ws: 8.16.0 dev: false /joycon@3.1.1: @@ -1816,7 +1849,7 @@ packages: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.0.0 - ws: 8.14.2 + ws: 8.16.0 xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -2190,6 +2223,12 @@ packages: entities: 4.5.0 dev: true + /partysocket@0.0.25: + resolution: {integrity: sha512-1oCGA65fydX/FgdnsiBh68buOvfxuteoZVSb3Paci2kRp/7lhF0HyA8EDb5X/O6FxId1e+usPTQNRuzFEvkJbQ==} + dependencies: + event-target-shim: 6.0.2 + dev: false + /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2708,6 +2747,13 @@ packages: ts-interface-checker: 0.1.13 dev: true + /superjson@1.13.3: + resolution: {integrity: sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==} + engines: {node: '>=10'} + dependencies: + copy-anything: 3.0.5 + dev: false + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -2966,6 +3012,11 @@ packages: requires-port: 1.0.0 dev: true + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: false + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: @@ -3237,8 +3288,8 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true - /ws@8.14.2: - resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} + /ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 diff --git a/scripts/dev.sh b/scripts/dev.sh new file mode 100755 index 0000000..02f78f9 --- /dev/null +++ b/scripts/dev.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# This script copies the source code of the packages in the packages/ directory +# to the node_modules/@statelyai/ directory of each destination project. + +# Load environment variables from .env file +if [ -f .env ]; then + export $(cat .env | xargs) +fi + +# Split the DEV_DESTINATIONS variable into an array +IFS=',' read -r -a destinations <<<"$DEV_DESTINATIONS" + +# Package name +package="inspect" + +for destination in "${destinations[@]}"; do + echo "Copying ${package} to ${destination}" + rm -rf "${destination}/node_modules/@statelyai/${package}" + cp -r "./" "${destination}/node_modules/@statelyai/${package}" +done diff --git a/src/createSkyInspector.ts b/src/createSkyInspector.ts new file mode 100644 index 0000000..db6080e --- /dev/null +++ b/src/createSkyInspector.ts @@ -0,0 +1,58 @@ +import PartySocket from 'partysocket'; +import { stringify } from 'superjson'; +import { v4 as uuidv4 } from 'uuid'; +import { createBrowserInspector } from './browser'; +import { + InspectorOptions, + createInspector as inspectCreator, +} from './createInspector'; +import { isNode } from './utils'; + +export function createSkyInspector( + options: { + apiKey?: string; // Not used yet, will be used to add additional premium features later + onerror?: (error: Error) => void; + } & InspectorOptions = {} +): ReturnType { + const { host, apiBaseURL } = { + host: 'localhost:1999', // 'stately-sky-beta.mellson.partykit.dev' + apiBaseURL: 'http://localhost:3000/registry/api/sky', // 'https://stately.ai/registry/api/sky', + }; + const server = apiBaseURL.replace('/api/sky', ''); + const { apiKey, onerror, ...inspectorOptions } = options; + const sessionId = uuidv4(); // Generate a unique session ID + const room = `inspect-${sessionId}`; + const socket = new PartySocket({ + host, + room, + WebSocket: isNode ? require('isomorphic-ws') : undefined, + }); + const liveInspectUrl = `${server}/inspect/${sessionId}`; + socket.onerror = onerror ?? console.error; + socket.onopen = () => { + console.log('Connected to Sky, link to your live inspect session:'); + console.log(liveInspectUrl); + }; + if (isNode) { + return inspectCreator({ + ...inspectorOptions, + send(event) { + const skyEvent = apiKey ? { apiKey, ...event } : event; + socket.send(stringify(skyEvent)); + }, + }); + } else { + const { filter, serialize } = inspectorOptions; + return createBrowserInspector({ + ...inspectorOptions, + url: liveInspectUrl, + serialize(event) { + if (!filter || filter(event)) { + const skyEvent = apiKey ? { apiKey, ...event } : event; + socket.send(stringify(skyEvent)); + } + return serialize ? serialize(event) : event; + }, + }); + } +} diff --git a/src/index.ts b/src/index.ts index 63bb702..c1d1629 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,10 @@ -export { createInspector } from './createInspector'; -export { createWebSocketInspector, createWebSocketReceiver } from './webSocket'; export { createBrowserInspector, createBrowserReceiver } from './browser'; +export { createInspector } from './createInspector'; +export { createSkyInspector } from './createSkyInspector'; export type { StatelyActorEvent, - StatelyInspectionEvent, StatelyEventEvent, + StatelyInspectionEvent, StatelySnapshotEvent, } from './types'; +export { createWebSocketInspector, createWebSocketReceiver } from './webSocket'; diff --git a/src/utils.ts b/src/utils.ts index 03eb42c..83ddeb8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import type { AnyEventObject, AnyActorRef } from 'xstate'; +import type { AnyActorRef, AnyEventObject } from 'xstate'; export function toEventObject(event: AnyEventObject | string): AnyEventObject { if (typeof event === 'string') { @@ -16,3 +16,8 @@ export function isActorRef(actorRef: any): actorRef is AnyActorRef { typeof actorRef.send === 'function' ); } + +export const isNode = + typeof process !== 'undefined' && + typeof process.versions?.node !== 'undefined' && + typeof document === 'undefined'; From aea1ce58694aa4ba0a0e464e868b7b9c2ea08a39 Mon Sep 17 00:00:00 2001 From: Anders Bech Mellson Date: Fri, 26 Jan 2024 13:02:16 +0100 Subject: [PATCH 2/5] update URLs --- src/createSkyInspector.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/createSkyInspector.ts b/src/createSkyInspector.ts index db6080e..2f989e0 100644 --- a/src/createSkyInspector.ts +++ b/src/createSkyInspector.ts @@ -15,8 +15,8 @@ export function createSkyInspector( } & InspectorOptions = {} ): ReturnType { const { host, apiBaseURL } = { - host: 'localhost:1999', // 'stately-sky-beta.mellson.partykit.dev' - apiBaseURL: 'http://localhost:3000/registry/api/sky', // 'https://stately.ai/registry/api/sky', + host: 'stately-sky-beta.mellson.partykit.dev', // 'localhost:1999' + apiBaseURL: 'https://stately.ai/registry/api/sky', // 'http://localhost:3000/registry/api/sky', }; const server = apiBaseURL.replace('/api/sky', ''); const { apiKey, onerror, ...inspectorOptions } = options; From 8b1964d51e84abc279e649473e0d0aff8a05e12d Mon Sep 17 00:00:00 2001 From: Anders Bech Mellson Date: Fri, 26 Jan 2024 16:22:24 +0100 Subject: [PATCH 3/5] Address PR feedback --- src/browser.ts | 23 +++++++++++++++++------ src/createSkyInspector.ts | 21 ++++++++++++--------- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/browser.ts b/src/browser.ts index cc8e0b2..c8621e6 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,7 +1,7 @@ +import safeStringify from 'fast-safe-stringify'; import { AnyEventObject, Observer, Subscribable, toObserver } from 'xstate'; -import { Adapter, Inspector, StatelyInspectionEvent } from './types'; import { InspectorOptions, createInspector } from './createInspector'; -import safeStringify from 'fast-safe-stringify'; +import { Adapter, Inspector, StatelyInspectionEvent } from './types'; import { UselessAdapter } from './useless'; interface BrowserReceiver extends Subscribable {} @@ -33,12 +33,16 @@ export interface BrowserInspectorOptions extends InspectorOptions { iframe?: HTMLIFrameElement | null; } +interface OptionalBrowserInspectorOptions { + send?: Adapter['send']; +} + /** * Creates a browser-based inspector that sends events to a remote inspector window. * The remote inspector opens an inspector window at the specified URL by default. */ export function createBrowserInspector( - options?: BrowserInspectorOptions + options?: BrowserInspectorOptions & OptionalBrowserInspectorOptions ): Inspector { const resolvedWindow = options?.window ?? (typeof window === 'undefined' ? undefined : window); @@ -57,7 +61,8 @@ export function createBrowserInspector( iframe: null, ...options, window: resolvedWindow, - } satisfies Required; + } satisfies Required & + OptionalBrowserInspectorOptions; const adapter = new BrowserAdapter(resolvedOptions); const inspector = createInspector(adapter, resolvedOptions); @@ -137,7 +142,10 @@ export class BrowserAdapter implements Adapter { private deferredEvents: StatelyInspectionEvent[] = []; public targetWindow: Window | null = null; - constructor(public options: Required) {} + constructor( + public options: Required & + OptionalBrowserInspectorOptions + ) {} public start() { this.targetWindow = this.options.iframe ? null @@ -173,10 +181,13 @@ export class BrowserAdapter implements Adapter { return; } - if (this.status === 'connected') { + if (this.options.send) { + this.options.send(event); + } else if (this.status === 'connected') { const serializedEvent = this.options.serialize(event); this.targetWindow?.postMessage(serializedEvent, '*'); } + this.deferredEvents.push(event); } } diff --git a/src/createSkyInspector.ts b/src/createSkyInspector.ts index 2f989e0..c0bf53f 100644 --- a/src/createSkyInspector.ts +++ b/src/createSkyInspector.ts @@ -8,6 +8,9 @@ import { } from './createInspector'; import { isNode } from './utils'; +// Not the most elegant way to do this, but it makes it much easier to test local changes +const isDevMode = false; + export function createSkyInspector( options: { apiKey?: string; // Not used yet, will be used to add additional premium features later @@ -15,8 +18,12 @@ export function createSkyInspector( } & InspectorOptions = {} ): ReturnType { const { host, apiBaseURL } = { - host: 'stately-sky-beta.mellson.partykit.dev', // 'localhost:1999' - apiBaseURL: 'https://stately.ai/registry/api/sky', // 'http://localhost:3000/registry/api/sky', + host: isDevMode + ? 'localhost:1999' + : 'stately-sky-beta.mellson.partykit.dev', + apiBaseURL: isDevMode + ? 'http://localhost:3000/registry/api/sky' + : 'https://stately.ai/registry/api/sky', }; const server = apiBaseURL.replace('/api/sky', ''); const { apiKey, onerror, ...inspectorOptions } = options; @@ -42,16 +49,12 @@ export function createSkyInspector( }, }); } else { - const { filter, serialize } = inspectorOptions; return createBrowserInspector({ ...inspectorOptions, url: liveInspectUrl, - serialize(event) { - if (!filter || filter(event)) { - const skyEvent = apiKey ? { apiKey, ...event } : event; - socket.send(stringify(skyEvent)); - } - return serialize ? serialize(event) : event; + send(event) { + const skyEvent = apiKey ? { apiKey, ...event } : event; + socket.send(stringify(skyEvent)); }, }); } From cc72918f1f491e56c3dc1c8520ca10b84ca3a7dd Mon Sep 17 00:00:00 2001 From: Anders Bech Mellson Date: Fri, 26 Jan 2024 16:22:41 +0100 Subject: [PATCH 4/5] Update src/createSkyInspector.ts Co-authored-by: David Khourshid --- src/createSkyInspector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/createSkyInspector.ts b/src/createSkyInspector.ts index c0bf53f..97e7b1a 100644 --- a/src/createSkyInspector.ts +++ b/src/createSkyInspector.ts @@ -13,7 +13,7 @@ const isDevMode = false; export function createSkyInspector( options: { - apiKey?: string; // Not used yet, will be used to add additional premium features later + apiKey?: string; // Not used yet, will be used to add additional features later onerror?: (error: Error) => void; } & InspectorOptions = {} ): ReturnType { From 115fc4666f049b46139aa8c9e2e6234d5d24c0fe Mon Sep 17 00:00:00 2001 From: Anders Bech Mellson Date: Mon, 29 Jan 2024 16:42:53 +0100 Subject: [PATCH 5/5] merge latest changes from main --- pnpm-lock.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ae1e3b..1fac615 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: partysocket: specifier: ^0.0.25 version: 0.0.25 + safe-stable-stringify: + specifier: ^2.4.3 + version: 2.4.3 superjson: specifier: ^1 version: 1.13.3 @@ -2505,6 +2508,11 @@ packages: is-regex: 1.1.4 dev: true + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + dev: false + /safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} dev: true