├── .gitignore
├── README.md
├── examples
├── broadcast-channel
│ ├── index.html
│ └── main.ts
├── event-bus
│ ├── index.html
│ └── main.ts
├── web-socket
│ ├── index.html
│ └── main.ts
└── web-worker
│ ├── index.html
│ ├── main.ts
│ ├── transferable
│ ├── index.html
│ ├── main.ts
│ └── worker.ts
│ └── worker.ts
├── package.json
├── src
├── async
│ └── from-dynamic-import.ts
├── event-bus
│ ├── from-broadcast-channel.ts
│ ├── from-event-bus.ts
│ └── from-event-listener.ts
├── index.ts
├── real-time
│ ├── from-event-source.ts
│ ├── from-web-socket.ts
│ └── from-webrtc.ts
└── workers
│ ├── from-audio-worklet.ts
│ ├── from-service-worker.ts
│ ├── from-shared-worker.ts
│ └── from-web-worker.ts
├── tsconfig.json
├── vite-env.d.ts
├── vite.config.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xstate-behaviors
2 |
3 | > Making web workers, web sockets, WebRTC, and other web-based APIs first-class actors in `xstate`.
4 |
5 | _This package is not yet release as it is in a very early alpha state. If you would like to use any of these helpers now its recommended to copy and paste them into your codebase for the time being!_
6 |
7 | If you have any ideas for other Web APIs to support please file an issue!
8 |
9 | ## Helper functions
10 |
11 | Any of the helper functions exported from this library can be used to invoke or spawn an actor that wraps around a certain Web API.
12 |
13 | ### Async
14 |
15 | - `fromDynamicImport`
16 | - Invoke/spawn a machine that is dynamically imported. Events sent to the machine while the dynamic import are loading are deferred until the machine is started.
17 |
18 | ### Workers
19 |
20 | _It is assumed that events sent to/from the worker are of the type `EventObject`, otherwise they are ignored._
21 |
22 | - `fromWebWorker`
23 | - Invoke/spawn a web worker.
24 | - `interpretInWebWorker`
25 | - Interpret a machine inside a web worker whose _parent_ is on the main thread. This means that you can use `sendParent` in your machine to send events out of the web worker.
26 | - `fromSharedWorker`
27 | - Invoke/spawn a shared worker.
28 | - `interpretInSharedWorker`
29 | - Interpret a machine inside a shared worker whose _parent_ is on a main thread. This means that you can use `sendParent` in your machine to send events out of the web worker.
30 | - `fromServiceWorker`
31 | - Invoke/spawn an actor that lets you communicate with a service worker. It is assumed that events sent from the worker are of the type `EventObject`.
32 | - `fromAudioWorklet`
33 | - Invoke/spawn a `AudioWorklet`.
34 |
35 | ### Real-time
36 |
37 | _It is assumed that events sent over the network are of the type `EventObject`, otherwise they are ignored._
38 |
39 | - `fromWebSocket`
40 | - Invoke/spawn a web socket.
41 | - `fromEventSource`
42 | - Invoke/spawn a event source for server side events.
43 | - `fromWebRTC` (TODO)
44 | - Still figuring this out let me know if you have any idea!
45 |
46 | ### Events
47 |
48 | - `fromEventBus`
49 | - Invoke/spawn an event bus so non-hierarchical actors can communicate
50 | - `EventBus` is a class that encapsulates an event bus. This is not a web API.
51 | - `fromBroadCastChannel`
52 | - Invoke/spawn a `BroadcastChannel` to communicate with anyone else listening
53 | - `fromEventListener`
54 | - Invoke/spawn an event from
55 | - `fromPostMessage` (TODO)
56 | - `fromMessageChannel` (TODO)
57 |
58 | ## TODO
59 |
60 | - [ ] Finish building out planned helpers.
61 | - [ ] Add ability to filter out unwanted events.
62 | - [ ] Add more tests/examples.
63 | - [ ] Build out documentation.
64 |
--------------------------------------------------------------------------------
/examples/broadcast-channel/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/broadcast-channel/main.ts:
--------------------------------------------------------------------------------
1 | import { createMachine, interpret, send } from 'xstate';
2 | import { log } from 'xstate/lib/actions';
3 | import { fromBroadcastChannel } from '@src';
4 |
5 | const BROADCAST_CHANNEL_NAME = 'BC';
6 |
7 | const pingMachine = createMachine({
8 | id: 'ping',
9 | invoke: {
10 | id: 'event bus',
11 | src: fromBroadcastChannel(() => new BroadcastChannel(BROADCAST_CHANNEL_NAME)),
12 | },
13 | on: {
14 | PONG: {
15 | actions: [log('PONG'), send({ type: 'PING' }, { to: 'event bus', delay: 1000 })],
16 | },
17 | },
18 | });
19 |
20 | const pongMachine = createMachine({
21 | id: 'pong',
22 | invoke: {
23 | id: 'event bus',
24 | src: fromBroadcastChannel(() => new BroadcastChannel(BROADCAST_CHANNEL_NAME)),
25 | },
26 | on: {
27 | PING: {
28 | actions: [log('PING'), send({ type: 'PONG' }, { to: 'event bus', delay: 1000 })],
29 | },
30 | },
31 | });
32 |
33 | const parentMachine = createMachine({
34 | id: 'parent',
35 | invoke: [
36 | { id: 'ping', src: pingMachine },
37 | { id: 'pong', src: pongMachine },
38 | ],
39 | entry: send({ type: 'PING' }, { to: 'pong' }),
40 | });
41 |
42 | const service = interpret(parentMachine).start();
43 |
--------------------------------------------------------------------------------
/examples/event-bus/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/event-bus/main.ts:
--------------------------------------------------------------------------------
1 | import { createMachine, interpret, send } from 'xstate';
2 | import { log } from 'xstate/lib/actions';
3 | import { EventBus, fromEventBus } from '@src';
4 |
5 | type Context = {
6 | eventBus: EventBus;
7 | };
8 |
9 | const pingMachine = createMachine({
10 | id: 'ping',
11 | invoke: {
12 | id: 'event bus',
13 | src: fromEventBus((context) => context.eventBus),
14 | },
15 | on: {
16 | PONG: {
17 | actions: [log('PONG'), send({ type: 'PING' }, { to: 'event bus', delay: 1000 })],
18 | },
19 | },
20 | });
21 |
22 | const pongMachine = createMachine({
23 | id: 'pong',
24 | invoke: {
25 | id: 'event bus',
26 | src: fromEventBus((context) => context.eventBus),
27 | },
28 | on: {
29 | PING: {
30 | actions: [log('PING'), send({ type: 'PONG' }, { to: 'event bus', delay: 1000 })],
31 | },
32 | },
33 | });
34 |
35 | const parentMachine = createMachine({
36 | id: 'parent',
37 | context: {
38 | eventBus: new EventBus('EV'),
39 | },
40 | invoke: [
41 | { id: 'ping', src: pingMachine, data: (context) => context },
42 | { id: 'pong', src: pongMachine, data: (context) => context },
43 | ],
44 | entry: send({ type: 'PING' }, { to: 'pong' }),
45 | });
46 |
47 | const service = interpret(parentMachine).start();
48 |
--------------------------------------------------------------------------------
/examples/web-socket/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/web-socket/main.ts:
--------------------------------------------------------------------------------
1 | import { createMachine, interpret, send } from 'xstate';
2 | import { fromWebSocket } from '@src';
3 |
4 | const pingMachine = createMachine({
5 | id: 'ping',
6 | invoke: {
7 | id: 'pong',
8 | src: fromWebSocket(() => new WebSocket('')),
9 | },
10 | entry: send({ type: 'PING' }, { to: 'pong' }),
11 | initial: 'active',
12 | states: {
13 | active: {
14 | on: {
15 | PONG: {
16 | actions: [send({ type: 'PING' }, { to: 'pong', delay: 1000 }), () => console.log('PONG')],
17 | },
18 | STOP: 'complete',
19 | },
20 | },
21 | complete: {},
22 | },
23 | });
24 |
25 | const service = interpret(pingMachine).start();
26 |
--------------------------------------------------------------------------------
/examples/web-worker/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/web-worker/main.ts:
--------------------------------------------------------------------------------
1 | import { createMachine, interpret, send, actions } from 'xstate';
2 | import { fromWebWorker } from '@src';
3 |
4 | const { log } = actions;
5 |
6 | const pingMachine = createMachine({
7 | id: 'ping',
8 | invoke: {
9 | id: 'pong',
10 | src: fromWebWorker(() => new Worker(new URL('./worker', import.meta.url), { type: 'module' })),
11 | },
12 | entry: send({ type: 'PING' }, { to: 'pong' }),
13 | initial: 'active',
14 | states: {
15 | active: {
16 | on: {
17 | PONG: {
18 | actions: [log('PONG'), send({ type: 'PING' }, { to: 'pong', delay: 1000 })],
19 | },
20 | STOP: 'complete',
21 | },
22 | },
23 | complete: {},
24 | },
25 | });
26 |
27 | const service = interpret(pingMachine).start();
28 |
--------------------------------------------------------------------------------
/examples/web-worker/transferable/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite App
8 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/examples/web-worker/transferable/main.ts:
--------------------------------------------------------------------------------
1 | import { createMachine, interpret, send, actions } from 'xstate';
2 | import { fromWebWorker } from '@src';
3 |
4 | const { log } = actions;
5 |
6 | type Context = {
7 | buffer: ArrayBuffer;
8 | };
9 |
10 | const pingMachine = createMachine({
11 | id: 'ping',
12 | context: {
13 | buffer: new ArrayBuffer(8),
14 | },
15 | invoke: {
16 | id: 'pong',
17 | src: fromWebWorker(() => new Worker(new URL('./worker', import.meta.url), { type: 'module' })),
18 | },
19 | entry: [
20 | log((ctx) => ctx.buffer.byteLength, 'Before transfer from main:'),
21 | send(({ buffer }) => ({ type: 'PING', buffer, _transfer: [buffer] }), { to: 'pong' }),
22 | // For some reason logging the buffer length will print 8, but the reference to the buffer shows that it was transferred
23 | log((ctx) => ctx.buffer, 'After transfer from main:'),
24 | ],
25 | initial: 'active',
26 | states: {
27 | active: {
28 | on: {
29 | PONG: { actions: log((_, e) => e.buffer.byteLength, 'After transfer from worker: ') },
30 | },
31 | },
32 | },
33 | });
34 |
35 | const service = interpret(pingMachine).start();
36 |
--------------------------------------------------------------------------------
/examples/web-worker/transferable/worker.ts:
--------------------------------------------------------------------------------
1 | import { createMachine, sendParent, actions } from 'xstate';
2 | import { interpretInWebWorker } from '@src';
3 |
4 | const { log } = actions;
5 |
6 | const pongMachine = createMachine({
7 | id: 'pong',
8 | on: {
9 | PING: {
10 | actions: [
11 | log((_, e) => e.buffer.byteLength, 'Before transfer from worker:'),
12 | sendParent((_, e) => ({ type: 'PONG', buffer: e.buffer, _transfer: [e.buffer] })),
13 | // For some reason logging the buffer length will print 8, but the reference to the buffer shows that it was transferred
14 | log((_, e) => e.buffer, 'After transfer from worker:'),
15 | ],
16 | },
17 | },
18 | });
19 |
20 | const service = interpretInWebWorker(pongMachine);
21 | service.start();
22 |
--------------------------------------------------------------------------------
/examples/web-worker/worker.ts:
--------------------------------------------------------------------------------
1 | import { createMachine, sendParent, actions } from 'xstate';
2 | import { interpretInWebWorker } from '@src';
3 |
4 | const { log } = actions;
5 |
6 | const pongMachine = createMachine({
7 | id: 'pong',
8 | on: {
9 | PING: {
10 | actions: [log('PING'), sendParent('PONG', { delay: 1000 })],
11 | },
12 | },
13 | });
14 |
15 | const service = interpretInWebWorker(pongMachine);
16 | service.start();
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.0.0",
3 | "scripts": {
4 | "dev": "vite",
5 | "build": "tsc && vite build",
6 | "serve": "vite preview"
7 | },
8 | "devDependencies": {
9 | "@types/dom-mediacapture-record": "^1.0.7",
10 | "@types/node": "^16.7.6",
11 | "@xstate/inspect": "^0.4.1",
12 | "typescript": "^4.2.3",
13 | "vite": "^2.3.5"
14 | },
15 | "dependencies": {
16 | "@types/three": "^0.129.0",
17 | "d3-random": "^2.2.2",
18 | "three": "^0.129.0",
19 | "xstate": "^4.23.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/async/from-dynamic-import.ts:
--------------------------------------------------------------------------------
1 | import {
2 | interpret,
3 | AnyInterpreter,
4 | StateMachine,
5 | InterpreterFrom,
6 | AnyEventObject,
7 | EventObject,
8 | StateSchema,
9 | InvokeCreator,
10 | } from 'xstate';
11 |
12 | /**
13 | * Create an invoked machine that is dynamically imported.
14 | * @param loadMachine Dynamically import a machine
15 | * @returns an invoke creator
16 | */
17 | export function fromDynamicImport<
18 | TContext,
19 | TEvent extends EventObject = AnyEventObject,
20 | Machine extends StateMachine = StateMachine<
21 | TContext,
22 | StateSchema,
23 | TEvent,
24 | any
25 | >
26 | >(
27 | loadMachine: (context: TContext, event: TEvent) => Promise
28 | ): InvokeCreator {
29 | return (context, event) => (sendBack, receive) => {
30 | let service: InterpreterFrom | null = null;
31 | let status: 'pending' | 'resolved' | 'rejected' | 'stopped' = 'pending';
32 | const pendingMessages: AnyEventObject[] = [];
33 |
34 | loadMachine(context, event)
35 | .then((machine) => {
36 | if (status === 'stopped') return;
37 |
38 | status = 'resolved';
39 | service = interpret(machine, {
40 | parent: { send: sendBack } as AnyInterpreter,
41 | }) as InterpreterFrom;
42 | service?.send(pendingMessages);
43 | })
44 | .catch(() => (status = 'rejected'));
45 |
46 | receive((event) => {
47 | if (status === 'pending') {
48 | pendingMessages.push(event);
49 | } else if (status === 'resolved' && service) {
50 | service?.send(event);
51 | }
52 | });
53 |
54 | return () => {
55 | status = 'stopped';
56 | service?.stop();
57 | };
58 | };
59 | }
60 |
--------------------------------------------------------------------------------
/src/event-bus/from-broadcast-channel.ts:
--------------------------------------------------------------------------------
1 | import { EventObject, AnyEventObject, InvokeCreator } from 'xstate';
2 | import { getEventType } from 'xstate/lib/utils';
3 |
4 | /**
5 | * Create an invoked service for a BroadcastChannel.
6 | * @param createBroadcastChannel Create a BroadcastChannel
7 | * @returns an invoke creator
8 | */
9 | export function fromBroadcastChannel(
10 | createBroadcastChannel: (context: TContext, event: TEvent) => BroadcastChannel
11 | ): InvokeCreator {
12 | return (context, event) => (sendBack, receive) => {
13 | const channel = createBroadcastChannel(context, event);
14 |
15 | const handler = (event: MessageEvent) => {
16 | try {
17 | // Will error out if the data is not a valid event
18 | getEventType(event.data);
19 | sendBack(event.data);
20 | } catch {}
21 | };
22 |
23 | channel.addEventListener('message', handler);
24 |
25 | receive((event) => {
26 | channel.postMessage(event);
27 | });
28 |
29 | return () => {
30 | channel.removeEventListener('message', handler);
31 | channel.close();
32 | };
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/src/event-bus/from-event-bus.ts:
--------------------------------------------------------------------------------
1 | import { Event, EventObject, AnyEventObject, InvokeCreator, Subscription } from 'xstate';
2 |
3 | type Listener = (event: Event) => void;
4 |
5 | export class EventBus {
6 | state: 'running' | 'stopped' = 'running';
7 | listeners: Set> = new Set();
8 |
9 | constructor(readonly id: string) {}
10 |
11 | protected get isStopped() {
12 | return this.state === 'stopped';
13 | }
14 |
15 | subscribe(listener: Listener): Subscription {
16 | if (this.isStopped) return { unsubscribe: () => {} };
17 |
18 | this.listeners.add(listener);
19 |
20 | return {
21 | unsubscribe: () => this.listeners.delete(listener),
22 | };
23 | }
24 |
25 | send(event: Event, listenerToIgnore?: Listener) {
26 | if (this.isStopped) return;
27 |
28 | for (const listener of this.listeners) {
29 | if (listener !== listenerToIgnore) listener(event);
30 | }
31 | }
32 |
33 | stop() {
34 | if (this.isStopped) return;
35 |
36 | this.state = 'stopped';
37 | this.listeners.clear();
38 | }
39 | }
40 |
41 | /**
42 | * Create an invoked service for a event bus.
43 | * @param createEventBus Create a EventBus
44 | * @returns an invoke creator
45 | */
46 | export function fromEventBus(
47 | createEventBus: (context: TContext, event: TEvent) => EventBus
48 | ): InvokeCreator {
49 | return (context, event) => (sendBack, receive) => {
50 | const bus = createEventBus(context, event);
51 |
52 | const listener: Listener = (event) => {
53 | sendBack(event);
54 | };
55 |
56 | const subscription = bus.subscribe(listener);
57 |
58 | receive((event) => {
59 | bus.send(event, listener);
60 | });
61 |
62 | return () => {
63 | subscription.unsubscribe();
64 | };
65 | };
66 | }
67 |
--------------------------------------------------------------------------------
/src/event-bus/from-event-listener.ts:
--------------------------------------------------------------------------------
1 | import { InvokeCallback } from 'xstate';
2 |
3 | export function fromEventListener(
4 | eventName: E
5 | ): () => InvokeCallback {
6 | return () => (sendBack) => {
7 | addEventListener(eventName, (e) => sendBack(e));
8 | return () => {
9 | removeEventListener(eventName, (e) => sendBack(e));
10 | };
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | // Async
2 | export { fromDynamicImport } from './async/from-dynamic-import';
3 |
4 | // Workers
5 | export { fromWebWorker, interpretInWebWorker } from './workers/from-web-worker';
6 | export { fromSharedWorker, interpretInSharedWorker } from './workers/from-shared-worker';
7 | export { fromServiceWorker } from './workers/from-service-worker';
8 | export { fromAudioWorklet } from './workers/from-audio-worklet';
9 |
10 | // Realtime
11 | export { fromWebSocket } from './real-time/from-web-socket';
12 | export { fromWebRTC } from './real-time/from-webrtc';
13 | export { fromEventSource } from './real-time/from-event-source';
14 |
15 | // Event Bus
16 | export { EventBus, fromEventBus } from './event-bus/from-event-bus';
17 | export { fromBroadcastChannel } from './event-bus/from-broadcast-channel';
18 | export { fromEventListener } from './event-bus/from-event-listener';
19 |
--------------------------------------------------------------------------------
/src/real-time/from-event-source.ts:
--------------------------------------------------------------------------------
1 | import { AnyEventObject, EventObject, InvokeCreator } from 'xstate';
2 | import { getEventType } from 'xstate/lib/utils';
3 |
4 | export function fromEventSource(
5 | createEventSource: (context: TContext, event: TEvent) => EventSource
6 | ): InvokeCreator {
7 | return (context, event) => (sendBack) => {
8 | const eventSource = createEventSource(context, event);
9 |
10 | eventSource.addEventListener('message', (event: MessageEvent) => {
11 | try {
12 | // Will error out if the data is not a valid event
13 | getEventType(event.data);
14 | sendBack(event.data);
15 | } catch {}
16 | });
17 |
18 | // TODO
19 | // eventSource.addEventListener('error', (event) => {
20 | // sendBack({ type: 'error', data: event });
21 | // });
22 |
23 | return () => {
24 | eventSource.close();
25 | };
26 | };
27 | }
28 |
--------------------------------------------------------------------------------
/src/real-time/from-web-socket.ts:
--------------------------------------------------------------------------------
1 | import { AnyEventObject, EventObject, InvokeCreator } from 'xstate';
2 | import { getEventType } from 'xstate/lib/utils';
3 |
4 | export function fromWebSocket(
5 | createSocket: (context: TContext, event: TEvent) => WebSocket
6 | ): InvokeCreator {
7 | return (context, event) => (sendBack, receive) => {
8 | const socket = createSocket(context, event);
9 |
10 | socket.addEventListener('message', (event: MessageEvent) => {
11 | try {
12 | // Will error out if the data is not a valid event
13 | const data = JSON.parse(event.data);
14 | getEventType(data);
15 | sendBack(data);
16 | } catch {}
17 | });
18 |
19 | // TODO
20 | // socket.addEventListener('error', (event) => {
21 | // sendBack({ type: 'error', data: event });
22 | // });
23 |
24 | receive((event) => {
25 | socket.send(JSON.stringify(event));
26 | });
27 |
28 | return () => {
29 | socket.close();
30 | };
31 | };
32 | }
33 |
--------------------------------------------------------------------------------
/src/real-time/from-webrtc.ts:
--------------------------------------------------------------------------------
1 | import { EventObject, AnyEventObject, InvokeCreator } from 'xstate';
2 | import { getEventType } from 'xstate/lib/utils';
3 |
4 | // TODO: figure out how webRTC works
5 | export function fromWebRTC(
6 | createWebRTC: (context: TContext, event: TEvent) => any
7 | ): InvokeCreator {
8 | return (context, event) => (sendBack, receive) => {};
9 | }
10 |
--------------------------------------------------------------------------------
/src/workers/from-audio-worklet.ts:
--------------------------------------------------------------------------------
1 | import { EventObject, AnyEventObject, InvokeCreator } from 'xstate';
2 | import { getEventType } from 'xstate/lib/utils';
3 |
4 | export function fromAudioWorklet(
5 | createAudioWorkletNode: (context: TContext, event: TEvent) => AudioWorkletNode
6 | ): InvokeCreator {
7 | return (context, event) => (sendBack, receive) => {
8 | const node = createAudioWorkletNode(context, event);
9 |
10 | const handler = (event: MessageEvent) => {
11 | try {
12 | // Will error out if the data is not a valid event
13 | getEventType(event.data);
14 | sendBack(event.data);
15 | } catch {}
16 | };
17 |
18 | node.port.addEventListener('message', handler);
19 |
20 | receive(({ _transfer, ...event }) => {
21 | node.port.postMessage(event, { transfer: _transfer });
22 | });
23 |
24 | return () => {
25 | node.port.removeEventListener('message', handler);
26 | node.port.close();
27 | };
28 | };
29 | }
30 |
--------------------------------------------------------------------------------
/src/workers/from-service-worker.ts:
--------------------------------------------------------------------------------
1 | import { AnyEventObject, EventObject, InvokeCreator } from 'xstate';
2 | import { getEventType } from 'xstate/lib/utils';
3 |
4 | export function fromServiceWorker<
5 | TContext,
6 | TEvent extends EventObject = AnyEventObject
7 | >(): InvokeCreator {
8 | return () => (sendBack, receive) => {
9 | const handler = (event: MessageEvent) => {
10 | try {
11 | // Will error out if the data is not a valid event
12 | getEventType(event.data);
13 | sendBack(event.data);
14 | } catch {}
15 | };
16 | navigator.serviceWorker.addEventListener('message', handler);
17 |
18 | receive(({ _transfer, ...event }) => {
19 | navigator.serviceWorker.controller?.postMessage(event, _transfer);
20 | });
21 |
22 | return () => {
23 | navigator.serviceWorker.removeEventListener('message', handler);
24 | };
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/src/workers/from-shared-worker.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AnyEventObject,
3 | AnyInterpreter,
4 | DefaultContext,
5 | EventObject,
6 | interpret,
7 | InterpreterOptions,
8 | InvokeCreator,
9 | StateMachine,
10 | StateSchema,
11 | Typestate,
12 | } from 'xstate';
13 | import { getEventType } from 'xstate/lib/utils';
14 |
15 | export function fromSharedWorker(
16 | createWorker: (context: TContext, event: TEvent) => SharedWorker
17 | ): InvokeCreator {
18 | return (context, event) => (sendBack, receive) => {
19 | const worker = createWorker(context, event);
20 |
21 | worker.port.start();
22 |
23 | const handler = (event: MessageEvent) => {
24 | try {
25 | // Will error out if the data is not a valid event
26 | getEventType(event.data);
27 | sendBack(event.data);
28 | } catch {}
29 | };
30 |
31 | worker.port.addEventListener('message', handler);
32 |
33 | receive(({ _transfer, ...event }) => {
34 | worker.port.postMessage(event, _transfer);
35 | });
36 |
37 | return () => {
38 | worker.port.removeEventListener('message', handler);
39 | worker.port.close();
40 | };
41 | };
42 | }
43 |
44 | export function interpretInSharedWorker<
45 | TContext = DefaultContext,
46 | TStateSchema extends StateSchema = any,
47 | TEvent extends EventObject = EventObject,
48 | TTypestate extends Typestate = { value: any; context: TContext }
49 | >(
50 | machine: StateMachine,
51 | options?: Partial
52 | ) {
53 | // TODO type as WorkerGlobalScope
54 | const _self = self as any;
55 |
56 | const service = interpret(machine, {
57 | ...options,
58 | deferEvents: true,
59 | parent: {
60 | send: ({ _transfer, ...event }) => {
61 | _self.postMessage(event, _transfer);
62 | },
63 | } as AnyInterpreter, // should probably be a different type
64 | });
65 |
66 | _self.addEventListener('connect' as any, (event: MessageEvent) => {
67 | const [port] = event.ports;
68 |
69 | port.addEventListener('message', (event: MessageEvent) => {
70 | try {
71 | // Will error out if the data is not a valid event
72 | getEventType(event.data);
73 | service.send(event.data);
74 | } catch {}
75 | });
76 | });
77 |
78 | return service;
79 | }
80 |
--------------------------------------------------------------------------------
/src/workers/from-web-worker.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AnyEventObject,
3 | AnyInterpreter,
4 | DefaultContext,
5 | EventObject,
6 | interpret,
7 | InterpreterOptions,
8 | InvokeCreator,
9 | StateMachine,
10 | StateSchema,
11 | Typestate,
12 | } from 'xstate';
13 | import { getEventType } from 'xstate/lib/utils';
14 |
15 | export function fromWebWorker(
16 | createWorker: (context: TContext, event: TEvent) => Worker
17 | ): InvokeCreator {
18 | return (context, event) => (sendBack, receive) => {
19 | const worker = createWorker(context, event);
20 | const handler = (event: MessageEvent) => {
21 | try {
22 | // Will error out if the data is not a valid event
23 | getEventType(event.data);
24 | sendBack(event.data);
25 | } catch {}
26 | };
27 | worker.addEventListener('message', handler);
28 |
29 | receive(({ _transfer, ...event }) => {
30 | worker.postMessage(event, _transfer);
31 | });
32 |
33 | return () => {
34 | worker.removeEventListener('message', handler);
35 | worker.terminate();
36 | };
37 | };
38 | }
39 |
40 | export function interpretInWebWorker<
41 | TContext = DefaultContext,
42 | TStateSchema extends StateSchema = any,
43 | TEvent extends EventObject = EventObject,
44 | TTypestate extends Typestate = { value: any; context: TContext }
45 | >(
46 | machine: StateMachine,
47 | options?: Partial
48 | ) {
49 | // TODO type as WorkerGlobalScope
50 | const _self = self as any;
51 |
52 | const service = interpret(machine, {
53 | ...options,
54 | deferEvents: true,
55 | parent: {
56 | send: ({ _transfer, ...event }) => {
57 | _self.postMessage(event, _transfer);
58 | },
59 | } as AnyInterpreter, // should probably be a different type
60 | });
61 |
62 | _self.addEventListener('message', (event: MessageEvent) => {
63 | try {
64 | // Will error out if the data is not a valid event
65 | getEventType(event.data);
66 | service.send(event.data);
67 | } catch {}
68 | });
69 |
70 | return service;
71 | }
72 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "target": "ESNext",
5 | "module": "ESNext",
6 | "lib": ["ESNext", "DOM"],
7 | "moduleResolution": "Node",
8 | "strict": true,
9 | "sourceMap": true,
10 | "resolveJsonModule": true,
11 | "esModuleInterop": true,
12 | "noEmit": true,
13 | "noUnusedLocals": true,
14 | "noUnusedParameters": true,
15 | "noImplicitReturns": true,
16 | "paths": {
17 | "@src": ["./src"]
18 | }
19 | },
20 | "include": ["src", "examples", "vite-env.d.ts"]
21 | }
22 |
--------------------------------------------------------------------------------
/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import path from 'path';
3 |
4 | export default defineConfig({
5 | resolve: {
6 | alias: {
7 | '@src': path.resolve(__dirname, '/src'),
8 | },
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@types/dom-mediacapture-record@^1.0.7":
6 | version "1.0.7"
7 | resolved "https://registry.yarnpkg.com/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.7.tgz#08bacca4296ef521d59049f43e65cf971bbf6be1"
8 | integrity sha512-ddDIRTO1ajtbxaNo2o7fPJggpN54PZf1ZUJKOjto2ENMJE/9GKUvaw3ZRuQzlS/p0E+PnIcssxfoqYJ4yiXSBw==
9 |
10 | "@types/node@^16.7.6":
11 | version "16.7.6"
12 | resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.6.tgz#8666478db8095aa66e25b7e469f3e7b53ea2855e"
13 | integrity sha512-VESVNFoa/ahYA62xnLBjo5ur6gPsgEE5cNRy8SrdnkZ2nwJSW0kJ4ufbFr2zuU9ALtHM8juY53VcRoTA7htXSg==
14 |
15 | "@types/three@^0.129.0":
16 | version "0.129.1"
17 | resolved "https://registry.yarnpkg.com/@types/three/-/three-0.129.1.tgz#e488e686132256dfd61cffd4d97ea6cf176a4a8d"
18 | integrity sha512-31VTcjAQNggIrCH9NVotTYsr5Ws/QMGGTaMK6RP3EgyzW2WEuZdm25TNidd6PJ3e4a6/hbswNacnTsjwc7ksbw==
19 |
20 | "@xstate/inspect@^0.4.1":
21 | version "0.4.1"
22 | resolved "https://registry.yarnpkg.com/@xstate/inspect/-/inspect-0.4.1.tgz#6b00b945c03280a333de8ddf10e91830c361052d"
23 | integrity sha512-tcGBUCVymP8ij0K2fXZckrwrQkmNJL1DgQAM0eSILQKb8iBDN3GeqhYkNCS4HP29EzF2TZL0+k74FPWKaU9Fhw==
24 | dependencies:
25 | fast-safe-stringify "^2.0.7"
26 |
27 | colorette@^1.2.2:
28 | version "1.2.2"
29 | resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
30 | integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
31 |
32 | d3-random@^2.2.2:
33 | version "2.2.2"
34 | resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-2.2.2.tgz#5eebd209ef4e45a2b362b019c1fb21c2c98cbb6e"
35 | integrity sha512-0D9P8TRj6qDAtHhRQn6EfdOtHMfsUWanl3yb/84C4DqpZ+VsgfI5iTVRNRbELCfNvRfpMr8OrqqUTQ6ANGCijw==
36 |
37 | esbuild@^0.12.5:
38 | version "0.12.7"
39 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.12.7.tgz#0243ff94b241d9c815720083633731bc5b601b24"
40 | integrity sha512-vxENKWBo928ErLiT/uUv8Sl2EoC5cF3cZzCTc8hDEh9ZAZ75xblJCr72NeJo74lxWaGopIePZPIWq1qDpLUHQQ==
41 |
42 | fast-safe-stringify@^2.0.7:
43 | version "2.0.7"
44 | resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743"
45 | integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==
46 |
47 | fsevents@~2.3.1:
48 | version "2.3.2"
49 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
50 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
51 |
52 | function-bind@^1.1.1:
53 | version "1.1.1"
54 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
55 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
56 |
57 | has@^1.0.3:
58 | version "1.0.3"
59 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
60 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
61 | dependencies:
62 | function-bind "^1.1.1"
63 |
64 | is-core-module@^2.2.0:
65 | version "2.4.0"
66 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1"
67 | integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==
68 | dependencies:
69 | has "^1.0.3"
70 |
71 | nanoid@^3.1.23:
72 | version "3.1.23"
73 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
74 | integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
75 |
76 | path-parse@^1.0.6:
77 | version "1.0.7"
78 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
79 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
80 |
81 | postcss@^8.3.0:
82 | version "8.3.0"
83 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.0.tgz#b1a713f6172ca427e3f05ef1303de8b65683325f"
84 | integrity sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==
85 | dependencies:
86 | colorette "^1.2.2"
87 | nanoid "^3.1.23"
88 | source-map-js "^0.6.2"
89 |
90 | resolve@^1.19.0:
91 | version "1.20.0"
92 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
93 | integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
94 | dependencies:
95 | is-core-module "^2.2.0"
96 | path-parse "^1.0.6"
97 |
98 | rollup@^2.38.5:
99 | version "2.51.1"
100 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.51.1.tgz#87bcd4095fe79b14c9bec0edc7ffa44e4827f793"
101 | integrity sha512-8xfDbAtBleXotb6qKEHWuo/jkn94a9dVqGc7Rwl3sqspCVlnCfbRek7ldhCARSi7h32H0xR4QThm1t9zHN+3uw==
102 | optionalDependencies:
103 | fsevents "~2.3.1"
104 |
105 | source-map-js@^0.6.2:
106 | version "0.6.2"
107 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
108 | integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
109 |
110 | three@^0.129.0:
111 | version "0.129.0"
112 | resolved "https://registry.yarnpkg.com/three/-/three-0.129.0.tgz#f5e530bbc96eac5d5b4749cb5da886ef0d42f554"
113 | integrity sha512-wiWio1yVRg2Oj6WEWsTHQo5eSzYpEwSBtPSi3OofNpvFbf26HFfb9kw4FZJNjII4qxzp0b1xLB11+tKkBGB1ZA==
114 |
115 | typescript@^4.2.3:
116 | version "4.3.2"
117 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
118 | integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==
119 |
120 | vite@^2.3.5:
121 | version "2.3.7"
122 | resolved "https://registry.yarnpkg.com/vite/-/vite-2.3.7.tgz#3023892419367465e1af1739578f8663d04243b2"
123 | integrity sha512-Y0xRz11MPYu/EAvzN94+FsOZHbSvO6FUvHv127CyG7mV6oDoay2bw+g5y9wW3Blf8OY3chaz3nc/DcRe1IQ3Nw==
124 | dependencies:
125 | esbuild "^0.12.5"
126 | postcss "^8.3.0"
127 | resolve "^1.19.0"
128 | rollup "^2.38.5"
129 | optionalDependencies:
130 | fsevents "~2.3.1"
131 |
132 | xstate@^4.23.0:
133 | version "4.23.1"
134 | resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.23.1.tgz#a85ce0ca4ad9737f1497e2da7a3abc6a5697fbcd"
135 | integrity sha512-8ZoCe8d6wDSPfkep+GBgi+fKAdMyXcaizoNf5FKceEhlso4+9n1TeK6oviaDsXZ3Z5O8xKkJOxXPNuD4cA9LCw==
136 |
--------------------------------------------------------------------------------