diff --git a/package-lock.json b/package-lock.json index 2232faf4..f3fcb95c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@commitlint/cli": "^17.7.2", "@commitlint/config-conventional": "^17.7.0", "@mui/icons-material": "^5.15.11", + "@reduxjs/toolkit": "^2.2.5", "@testing-library/react": "^14.1.2", "@types/jest": "^29.5.11", "@types/react": "^18.2.45", @@ -2491,6 +2492,45 @@ "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==", "dev": true }, + "node_modules/@reduxjs/toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.5.tgz", + "integrity": "sha512-aeFA/s5NCG7NoJe/MhmwREJxRkDs0ZaSqt0MxhWUrwCf1UQXpwR87RROJEql0uAkLI6U7snBOYOcKw83ew3FPg==", + "dev": true, + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "dev": true + }, + "node_modules/@reduxjs/toolkit/node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "dev": true, + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.0.tgz", @@ -6738,6 +6778,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -11540,6 +11590,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.6", "license": "MIT", @@ -15251,6 +15307,33 @@ "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==", "dev": true }, + "@reduxjs/toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.5.tgz", + "integrity": "sha512-aeFA/s5NCG7NoJe/MhmwREJxRkDs0ZaSqt0MxhWUrwCf1UQXpwR87RROJEql0uAkLI6U7snBOYOcKw83ew3FPg==", + "dev": true, + "requires": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "dependencies": { + "redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "dev": true + }, + "redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "dev": true, + "requires": {} + } + } + }, "@rollup/rollup-android-arm-eabi": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.0.tgz", @@ -18197,6 +18280,12 @@ "version": "5.2.4", "dev": true }, + "immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "dev": true + }, "import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -21463,6 +21552,12 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, + "reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "dev": true + }, "resolve": { "version": "1.22.6", "requires": { diff --git a/package.json b/package.json index 7c0599ad..6d17bd84 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@commitlint/cli": "^17.7.2", "@commitlint/config-conventional": "^17.7.0", "@mui/icons-material": "^5.15.11", + "@reduxjs/toolkit": "^2.2.5", "@testing-library/react": "^14.1.2", "@types/jest": "^29.5.11", "@types/react": "^18.2.45", @@ -76,10 +77,10 @@ "@emotion/styled": "^11.11.0", "@mui/material": "^5.15.11", "@types/mui-datatables": "*", + "@xstate/react": "^4.1.1", "mui-datatables": "*", "react": ">=17", "react-dom": ">=17", - "@xstate/react": "^4.1.1", "xstate": "^5.13.0" }, "peerDependenciesMeta": { diff --git a/src/actors/index.ts b/src/actors/index.ts index 0dffc848..2923105d 100644 --- a/src/actors/index.ts +++ b/src/actors/index.ts @@ -8,7 +8,28 @@ export { selectValidationResults } from './validators/dataValidator'; +export * from './worker'; + +export { + REDUX_COMMANDS, + REDUX_EVENTS, + reduxActor, + reduxCommands, + reduxEvents, + type REXUX_ACTOR_EVENTS +} from './reduxActor'; + +export { + RTK_EVENTS, + rtkQueryActor, + rtkQueryActorCommands, + rtkQueryActorEvents +} from './rtkQueryActor'; + +export const REEE = 'xxx'; + export { + DeferEvents, XSTATE_DEBUG_EVENT, deadLetter, forwardToActors, diff --git a/src/actors/reduxActor.ts b/src/actors/reduxActor.ts new file mode 100644 index 00000000..a4325b6e --- /dev/null +++ b/src/actors/reduxActor.ts @@ -0,0 +1,94 @@ +import { assertEvent, sendTo, setup } from 'xstate'; + +import { Selector, Store, UnknownAction } from '@reduxjs/toolkit'; + +interface ReduxActorContext { + store: Store; + selectors: { + [key: string]: Selector; + }; +} + +/* eslint-disable @typescript-eslint/no-explicit-any */ +type extraArgs = any[]; + +export const REDUX_COMMANDS = { + DISPATCH: 'DISPATCH', + GET_STATE: 'GET_STATE', + GET_STATE_FROM_SELECTOR: 'GET_STATE_FROM_SELECTOR', + SUBSCRIBE: 'SUBSCRIBE', + UNSUBSCRIBE: 'UNSUBSCRIBE' +}; + +export const REDUX_EVENTS = { + REDUX_STATE_SNAPSHOT: 'REDUX_STATE_SNAPSHOT' +}; + +export type REXUX_ACTOR_EVENTS = + | { type: 'DISPATCH'; data: { action: UnknownAction } } + | { type: 'SUBSCRIBE' } + | { type: 'UNSUBSCRIBE' } + | { type: 'REDUX_STATE_SNAPSHOT'; data: { snapshot: unknown } } + | { type: 'GET_STATE'; returnAddress: string; data: { key: string } } + | { + type: 'GET_STATE_FROM_SELECTOR'; + returnAddress: string; + data: { selector: string; extraArgs?: extraArgs }; + }; + +export const reduxEvents = { + stateSnapshot: (snapshot: unknown) => ({ + type: 'STATE_SNAPSHOT', + data: { snapshot } + }) +}; + +export const reduxCommands = { + dispatch: (action: UnknownAction) => ({ + type: 'DISPATCH', + data: { action } + }), + + getState: (key: string, returnAddress: string) => ({ + type: 'GET_STATE', + returnAddress, + data: { key } + }), + + getStateFromSelector: (selector: string, returnAddress: string, extraArgs?: extraArgs) => ({ + type: 'GET_STATE_FROM_SELECTOR', + returnAddress, + data: { selector, extraArgs } + }) +}; + +export const reduxActor = setup({ + types: { + context: {} as ReduxActorContext, + input: {} as ReduxActorContext, + events: {} as REXUX_ACTOR_EVENTS + } +}).createMachine({ + initial: 'idle', + context: ({ input }) => input, + states: { + idle: { + on: { + DISPATCH: { + actions: [({ event, context }) => context.store.dispatch(event.data.action)] + }, + GET_STATE_FROM_SELECTOR: { + actions: sendTo( + ({ event }) => event.returnAddress, + ({ context, event }) => { + assertEvent(event, 'GET_STATE_FROM_SELECTOR'); + const selector = context.selectors[event.data.selector]; + const snapshot = selector(context.store.getState(), ...(event.data.extraArgs || [])); + return reduxEvents.stateSnapshot(snapshot); + } + ) + } + } + } + } +}); diff --git a/src/actors/rtkQueryActor.ts b/src/actors/rtkQueryActor.ts new file mode 100644 index 00000000..7e701d99 --- /dev/null +++ b/src/actors/rtkQueryActor.ts @@ -0,0 +1,152 @@ +import { Store } from '@reduxjs/toolkit'; +import { Api, EndpointDefinitions, QueryResultSelectorResult } from '@reduxjs/toolkit/query/react'; +import { DoneActorEvent, ErrorActorEvent, EventObject, fromPromise, sendTo, setup } from 'xstate'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +interface RtkQueryActorContext { + api: Api; + store: Store; +} + +interface RtkQueryActorInput { + api: Api; + store: Store; +} + +interface initiateQueryEvent extends EventObject { + type: 'INITIATE_QUERY'; + data: { + endpointName: string; + params: unknown; + }; + returnAddress: string; +} + +interface queryDoneEvent extends DoneActorEvent { + /* eslint-disable @typescript-eslint/no-explicit-any */ + output: { + result: QueryResultSelectorResult; + sourceEvent: initiateQueryEvent; + }; +} + +interface queryFailedEvent extends ErrorActorEvent { + /* eslint-disable @typescript-eslint/no-explicit-any */ + error: { + result: QueryResultSelectorResult; + sourceEvent: initiateQueryEvent; + }; +} + +interface QueryResultEvent extends EventObject { + type: 'QUERY_RESULT'; + data: { + result: unknown; + }; +} + +interface QueryFailedEvent extends EventObject { + type: 'QUERY_FAILED'; + data: { + error: unknown; + }; +} + +export const rtkQueryActorCommands = { + initiateQuery: ({ + endpointName, + params, + returnAddress + }: { + endpointName: string; + params: unknown; + returnAddress: string; + }): initiateQueryEvent => ({ + type: 'INITIATE_QUERY', + data: { endpointName, params }, + returnAddress + }) +}; + +export const rtkQueryActorEvents = { + queryResult: ({ result }: { result: unknown }): QueryResultEvent => ({ + type: 'QUERY_RESULT', + data: { result } + }), + queryFailed: ({ error }: { error: unknown }): QueryFailedEvent => ({ + type: 'QUERY_FAILED', + data: { error } + }) +}; + +export const RTK_EVENTS = { + QUERY_RESULT: 'QUERY_RESULT', + QUERY_FAILED: 'QUERY_FAILED' +}; + +type EVENTS = initiateQueryEvent | queryDoneEvent | queryFailedEvent; + +interface InitiateQueryActorInput { + context: RtkQueryActorContext; + event: initiateQueryEvent; +} +const InitiateQueryActor = fromPromise(async ({ input }: { input: InitiateQueryActorInput }) => { + const { event, context } = input; + console.log('event.data.endpointName', event, context); + const result = await context.store.dispatch( + (context.api.endpoints[event.data.endpointName] as any).initiate(event.data.params) + ); + return { + result, + sourceEvent: event + }; +}); + +export const rtkQueryActor = setup({ + types: { + context: {} as RtkQueryActorContext, + input: {} as RtkQueryActorInput, + events: {} as EVENTS + }, + actors: { + InitiateQueryActor + } +}).createMachine({ + initial: 'idle', + context: ({ input }) => input, + states: { + idle: { + on: { + INITIATE_QUERY: { + target: 'querying' + } + } + }, + querying: { + invoke: { + id: 'initiateQuery', + input: ({ context, event }) => ({ + context, + event: event as initiateQueryEvent + }), + src: 'InitiateQueryActor', + onDone: { + actions: sendTo( + ({ event }) => event.output.sourceEvent.returnAddress, + ({ event }) => rtkQueryActorEvents.queryResult({ result: event.output }) + ), + target: 'idle' + }, + onError: { + actions: sendTo( + ({ event }) => (event as queryFailedEvent).error.sourceEvent.returnAddress, + ({ event }) => rtkQueryActorEvents.queryFailed({ error: event.error }) + ), + + target: 'idle' + } + } + } + } +}); diff --git a/src/actors/utils.ts b/src/actors/utils.ts index 1854b7da..1deb016a 100644 --- a/src/actors/utils.ts +++ b/src/actors/utils.ts @@ -1,7 +1,7 @@ // disbale stict no any for now for full file /* eslint-disable @typescript-eslint/no-explicit-any */ -import { AnyActorRef, AnyEventObject, enqueueActions, sendTo } from 'xstate'; +import { AnyActorRef, AnyEventObject, assign, enqueueActions, sendTo } from 'xstate'; import { AnyActorSystem } from 'xstate/dist/declarations/src/system'; type ContextWithReturnAddress = { returnAddress: AnyActorRef }; @@ -43,3 +43,35 @@ export const reply = (eventFn: (actionArgs: any, params: any) => AnyEventObject) sendTo(({ context }: { context: ContextWithReturnAddress }) => context.returnAddress, eventFn); export const XSTATE_DEBUG_EVENT = 'XSTATE_DEBUG_EVENT'; + +type deferredEventsQueue = AnyEventObject[]; + +interface DeferredEventsQueueContext { + deferredEventsQueue: deferredEventsQueue; +} + +interface deferActionParams { + event: AnyEventObject; + context: DeferredEventsQueueContext; +} + +const defer = assign({ + deferredEventsQueue: ({ context: { deferredEventsQueue }, event }: deferActionParams) => [ + ...deferredEventsQueue, + event + ] +}); + +const recall = enqueueActions(({ context: { deferredEventsQueue }, enqueue }) => { + enqueue.assign({ + deferredEventsQueue: [] + }); + for (const event of deferredEventsQueue) { + enqueue.raise(event); + } +}); + +export const DeferEvents = { + defer, + recall +}; diff --git a/src/actors/validators/dataValidator.ts b/src/actors/validators/dataValidator.ts index e7cbde67..50dc47b9 100644 --- a/src/actors/validators/dataValidator.ts +++ b/src/actors/validators/dataValidator.ts @@ -168,13 +168,14 @@ export const dataValidatorMachine = setup({ waiting: {}, debouncing: { after: { - debounceTimeout: '#validationMachine.validatingData' + debounceTimeout: '#validatingData' } } } }, validatingData: { + id: 'validatingData', invoke: { src: 'ValidateActor', input: ({ context }: { context: ValidationMachineContext }) => ({ @@ -224,7 +225,7 @@ export const dataValidatorMachine = setup({ type ValidationMachineSnapshot = SnapshotFrom; export const selectValidationResults = (state: ValidationMachineSnapshot) => - state.context.validationResults; + state.context?.validationResults; export const selectIsValidating = (state: ValidationMachineSnapshot) => state.matches('validatingData'); diff --git a/src/actors/worker/README.md b/src/actors/worker/README.md new file mode 100644 index 00000000..e69de29b diff --git a/src/actors/worker/events.ts b/src/actors/worker/events.ts new file mode 100644 index 00000000..472120b9 --- /dev/null +++ b/src/actors/worker/events.ts @@ -0,0 +1,47 @@ +import { AnyEventObject } from 'xstate'; + +export const WORKER_COMMANDS = { + START_ACTOR: 'START_ACTOR', + STOP_ACTOR: 'STOP_ACTOR', + SEND_EVENT: 'SEND_EVENT', + GET_STATE: 'GET_STATE' +}; + +export const workerCommands = { + startActor: () => ({ type: WORKER_COMMANDS.START_ACTOR }), + stopActor: () => ({ type: WORKER_COMMANDS.STOP_ACTOR }), + sendEvent: (event: AnyEventObject) => ({ type: WORKER_COMMANDS.SEND_EVENT, event }), + getState: () => ({ type: WORKER_COMMANDS.GET_STATE }) +}; + +export interface PROXY_EVENT { + type: 'PROXY_EVENT'; + data: { + event: AnyEventObject; + to: string; + }; +} + +export interface STATE_SNAPSHOT_EVENT { + type: 'STATE_SNAPSHOT'; + data: { + snapshot: unknown; + }; +} + +export const workerEvents = { + proxyEvent: (event: AnyEventObject, to: string) => ({ + type: 'PROXY_EVENT', + data: { event, to } + }), + + stateSnapshot: (snapshot: unknown) => ({ + type: 'STATE_SNAPSHOT', + data: { snapshot } + }) +}; + +export const WORKER_EVENTS = { + STATE_SNAPSHOT: 'STATE_SNAPSHOT', + PROXY_EVENT: 'PROXY_EVENT' +}; diff --git a/src/actors/worker/fromWorkerfiedActor.ts b/src/actors/worker/fromWorkerfiedActor.ts new file mode 100644 index 00000000..4d3f7dd0 --- /dev/null +++ b/src/actors/worker/fromWorkerfiedActor.ts @@ -0,0 +1,120 @@ +import { + ActorLogic, + AnyEventObject, + AnyMachineSnapshot, + EventObject, + NonReducibleUnknown, + StateValue, + matchesState +} from 'xstate'; +import { AnyActorSystem } from 'xstate/dist/declarations/src/system'; +import { STATE_SNAPSHOT_EVENT, WORKER_EVENTS, workerCommands } from './events'; + +const instanceStates = /* #__PURE__ */ new WeakMap(); + +type WorkerInput = NonReducibleUnknown; + +type WorkerSnapshot = AnyMachineSnapshot; +type WorkerActorLogic = ActorLogic< + WorkerSnapshot, + TEvent, + TInput, + AnyActorSystem, + EventObject // TEmitted +>; + +interface ProxyEvent { + type: 'PROXY_EVENT'; + data: { + event: AnyEventObject; + to: string; + }; +} + +export const fromWorkerfiedActor = ( + worker: Worker +): WorkerActorLogic => ({ + config: Worker, + + start: (state, actorScope) => { + const { self, system } = actorScope; + console.log('Starting fromWorkerActor [+]', state, actorScope); + worker.postMessage(workerCommands.startActor()); + const workerState = { + worker, + snapshot: null + }; + + worker.addEventListener('message', (event) => { + const eventFromWorker = event.data as AnyEventObject; + if (eventFromWorker.type == 'STATE_SNAPSHOT') { + self.send(eventFromWorker); + return state; + } + + if (event.type === WORKER_EVENTS.PROXY_EVENT) { + const proxyEvent = event as ProxyEvent; + if (proxyEvent.data.to === 'parent' && self._parent) { + console.log('Relaying to parent', proxyEvent.data); + self._parent.send(proxyEvent.data.event); + return state; + } + + system.get(proxyEvent.data.to).send(proxyEvent.data.event); + return state; + } + }); + + instanceStates.set(self, workerState); + }, + transition: (state, event, actorScope) => { + const { self } = actorScope; + const workerState = instanceStates.get(self); + if (event.type === 'xstate.stop') { + console.log('Stopping fromWorkerActor...', state, event, actorScope); + workerState.worker.postMessage(workerCommands.stopActor()); + workerState.worker.terminate(); + return { + ...state, + status: 'stopped', + error: undefined + }; + } + if (event.type == WORKER_EVENTS.STATE_SNAPSHOT) { + const snapshot = (event as STATE_SNAPSHOT_EVENT).data.snapshot as AnyMachineSnapshot; + return { + ...state, + ...(snapshot || {}) + }; + } + + try { + workerState.worker.postMessage(workerCommands.sendEvent(event)); + } catch (error) { + console.error('Error sending event to worker', error, event); + } + const nextState = { + ...state + }; + return nextState; + }, + getInitialSnapshot: (_, input) => { + return { + status: 'active', + output: undefined, + error: undefined, + value: 'created', + input, + tags: [], + historyValue: undefined, + context: {}, + matches: function (value: StateValue) { + const currentValue = (this as WorkerSnapshot).value; + return matchesState(value, currentValue); + } + } as unknown as AnyMachineSnapshot; + }, + + getPersistedSnapshot: (snapshot) => snapshot, + restoreSnapshot: (snapshot) => snapshot as WorkerSnapshot +}); diff --git a/src/actors/worker/index.ts b/src/actors/worker/index.ts new file mode 100644 index 00000000..1f69094c --- /dev/null +++ b/src/actors/worker/index.ts @@ -0,0 +1,2 @@ +export { fromWorkerfiedActor } from './fromWorkerfiedActor'; +export { workerfyActor } from './workerfy'; diff --git a/src/actors/worker/workerfy.ts b/src/actors/worker/workerfy.ts new file mode 100644 index 00000000..5308eb7c --- /dev/null +++ b/src/actors/worker/workerfy.ts @@ -0,0 +1,92 @@ +import { AnyActorLogic, AnyActorRef, Subscription, createActor, setup } from 'xstate'; +import { WORKER_COMMANDS, workerEvents } from './events'; + +interface ProxyActorContext { + proxyToId: string; +} + +interface ProxyActorInput { + proxyToId: string; +} + +const ProxyActor = setup({ + types: { + context: {} as ProxyActorContext, + input: {} as ProxyActorInput + } +}).createMachine({ + id: 'proxy-actor', + initial: 'idle', + context: ({ input }) => ({ + proxyToId: input.proxyToId + }), + + states: { + idle: { + on: { + '*': { + actions: [ + ({ event, context }) => console.log('Proxying event', event, 'to', context.proxyToId), + ({ event, context }) => postMessage(workerEvents.proxyEvent(event, context.proxyToId)) + ] + } + } + } + } +}); + +const syncSnapshot = (actorRef: AnyActorRef) => { + return actorRef.subscribe((snapshot) => { + const jsonSnapshot = snapshot.toJSON(); + delete jsonSnapshot.children; // children are not serializable + try { + postMessage(workerEvents.stateSnapshot(jsonSnapshot)); + } catch (error) { + console.error('Error sending snapshot from worker', error, jsonSnapshot); + } + }); +}; + +export const workerfyActor = (actor: AnyActorLogic) => { + let actorRef: AnyActorRef | null = null; + let snapshotSubscription: Subscription | null = null; + const parentProxy = createActor(ProxyActor, { + input: { + proxyToId: 'parent' + } + }).start(); + + addEventListener('message', (event) => { + if (event.data.type === WORKER_COMMANDS.START_ACTOR) { + actorRef = createActor(actor, { + input: event.data.input, + parent: parentProxy + }); + + snapshotSubscription = syncSnapshot(actorRef); + actorRef.start(); + } + + if (event.data.type === WORKER_COMMANDS.STOP_ACTOR) { + snapshotSubscription?.unsubscribe && snapshotSubscription.unsubscribe(); + actorRef?.stop && actorRef.stop(); + } + + if (event.data.type === WORKER_COMMANDS.SEND_EVENT) { + if (!actorRef) { + throw new Error('Cannot send event to uninitialized actor'); + } + actorRef.send(event.data.event); + } + + if (event.data.type === WORKER_COMMANDS.GET_STATE) { + if (!actorRef) { + throw new Error('Cannot get state of uninitialized actor'); + } + const snapshot = actorRef.getSnapshot().toJSON(); + postMessage(workerEvents.stateSnapshot(snapshot)); + } + }); + + return actorRef; +};