Skip to content

Commit

Permalink
feat: add Svelte Store respondOnMount and Runtime respondToOutput
Browse files Browse the repository at this point in the history
  • Loading branch information
tdreyno committed May 6, 2023
1 parent 44db7b9 commit c1ba485
Show file tree
Hide file tree
Showing 14 changed files with 146 additions and 24 deletions.
4 changes: 4 additions & 0 deletions src/__tests__/loadingMachine/core/actions/World.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { type ActionCreatorType, createAction } from "../../../../action"

export const world = createAction("World")
export type World = ActionCreatorType<typeof world>
1 change: 1 addition & 0 deletions src/__tests__/loadingMachine/core/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./ReEnter"
export * from "./Reset"
export * from "./StartLoading"
export * from "./Update"
export * from "./World"
3 changes: 2 additions & 1 deletion src/__tests__/loadingMachine/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as Actions from "./actions"
import * as OutputActions from "./outputActions"
import States from "./states"

export { States, Actions }
export { States, Actions, OutputActions }
4 changes: 4 additions & 0 deletions src/__tests__/loadingMachine/core/outputActions/Hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { type ActionCreatorType, createAction } from "../../../../action"

export const hello = createAction("Hello")
export type Hello = ActionCreatorType<typeof hello>
2 changes: 2 additions & 0 deletions src/__tests__/loadingMachine/core/outputActions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Export local actions
export * from "./Hello"
15 changes: 10 additions & 5 deletions src/__tests__/loadingMachine/core/states/Ready.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { goBack, noop } from "../effects"

import { goBack } from "../effects"
import type { Enter } from "../../../../action"
import type { Reset } from "../actions"
import type { Reset, World } from "../actions"
import type { Shared } from "../types"
import { state } from "../../../../state"
import { output } from "../../../../effect"
import { hello } from "../outputActions"

type Actions = Enter | Reset // | ReEnter
type Actions = Enter | Reset | World // | ReEnter
type Data = [Shared]

export default state<Actions, Data>(
{
Enter: () => noop(),
Enter: () => output(hello()),

Reset: goBack,

World: ([shared], __, { update }) => {
return update([{ ...shared, didWorld: true }])
},

// ReEnter: (data, _, { reenter }) => reenter(data),
},
{ name: "Ready" },
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/loadingMachine/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export interface Shared {
message: string
didWorld?: boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createInitialContext } from "../context"
import { createRuntime } from "../runtime"
import { isState, state } from "../state"

describe("Ports", () => {
describe("onOutput", () => {
test("should transition through multiple states", async () => {
const enterAction = enter()

Expand Down
61 changes: 54 additions & 7 deletions src/__tests__/waitState.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,6 @@ const AfterThing = state<Enter, D>(

describe("waitState", () => {
it("should run a wait state", async () => {
const BeforeThing = state<Enter, D>(
{
Enter: data => WaitForThing([data, RETURN_COUNT]),
},
{ name: "BeforeThing" },
)

const WaitForThing = waitState(
fetchThing,
thingFetched,
Expand All @@ -42,6 +35,14 @@ describe("waitState", () => {
name: "WaitForThing",
},
)

const BeforeThing = state<Enter, D>(
{
Enter: data => WaitForThing([data, RETURN_COUNT]),
},
{ name: "BeforeThing" },
)

const context = createInitialContext([
BeforeThing({
count: INITIAL_COUNT,
Expand Down Expand Up @@ -169,4 +170,50 @@ describe("waitState", () => {
expect(isState(runtime.currentState(), TimedOutState)).toBeTruthy()
expect((runtime.currentState().data as D).count).toBe(INITIAL_COUNT)
})

it("should be able to use respondToOutput", async () => {
const WaitForThing = waitState(
fetchThing,
thingFetched,
(data: D, payload) => {
return AfterThing({ ...data, count: data.count + payload })
},
{
name: "WaitForThing",
},
)

const BeforeThing = state<Enter, D>(
{
Enter: data => WaitForThing([data, RETURN_COUNT]),
},
{ name: "BeforeThing" },
)

const context = createInitialContext([
BeforeThing({
count: INITIAL_COUNT,
}),
])

const runtime = createRuntime(context, { thingFetched }, { fetchThing })

runtime.respondToOutput("FetchThing", async payload => {
await timeout(250)
return thingFetched(payload)
})

expect(isState(runtime.currentState(), BeforeThing)).toBeTruthy()

await runtime.run(enter())

expect(isState(runtime.currentState(), WaitForThing)).toBeTruthy()

await timeout(500)

expect(isState(runtime.currentState(), AfterThing)).toBeTruthy()
expect((runtime.currentState().data as D).count).toBe(
INITIAL_COUNT + RETURN_COUNT,
)
})
})
24 changes: 20 additions & 4 deletions src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { Context } from "./context.js"
type ContextChangeSubscriber = (context: Context) => void
type OutputSubscriber<
OAM extends { [key: string]: (...args: Array<any>) => Action<any, any> },
> = (action: ReturnType<OAM[keyof OAM]>) => void
> = (action: ReturnType<OAM[keyof OAM]>) => void | Promise<void>

type QueueItem = {
onComplete: () => void
Expand Down Expand Up @@ -66,6 +66,22 @@ export class Runtime<
return () => this.#outputSubscribers.delete(fn)
}

respondToOutput<
T extends OAM["type"],
P extends Extract<OAM, { type: T }>["payload"],
A extends ReturnType<AM[keyof AM]>,
>(type: T, handler: (payload: P) => Promise<A> | A | void): () => void {
return this.onOutput(async output => {
if (output.type === type) {
const maybeAction = await handler(output.payload as P)

if (maybeAction) {
await this.run(maybeAction)
}
}
})
}

disconnect(): void {
this.#contextChangeSubscribers.clear()
}
Expand Down Expand Up @@ -139,9 +155,9 @@ export class Runtime<
if (item.label === "goBack") {
results = this.#handleGoBack()
} else if (item.label === "output") {
this.#outputSubscribers.forEach(sub =>
sub(item.data as ReturnType<OAM[keyof OAM]>),
)
this.#outputSubscribers.forEach(sub => {
void sub(item.data as ReturnType<OAM[keyof OAM]>)
})
} else {
this.#runEffect(item)
}
Expand Down
11 changes: 9 additions & 2 deletions src/svelte/__tests__/Comp.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
<script lang="ts">
import { machine } from './machine';
import { world } from "../../__tests__/loadingMachine/core/actions"
import { machine } from "./machine"
machine.respondOnMount("Hello", world)
</script>

<div data-testid="name">{$machine.currentState.name}</div>
<div data-testid="didWorld">
{$machine.currentState.data[0].didWorld ? "true" : "false"}
</div>

<div data-testid="name">{$machine.currentState.name}</div>
1 change: 1 addition & 0 deletions src/svelte/__tests__/machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export const machine = createStore(
Core.States,
Core.Actions,
Core.States.Initializing([{ message: "Loading" }, true]),
Core.OutputActions,
)
14 changes: 12 additions & 2 deletions src/svelte/__tests__/svelte.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,21 @@
import "@testing-library/jest-dom/extend-expect"

import Comp from "./Comp.svelte"
import { render } from "@testing-library/svelte"
import { act, render } from "@testing-library/svelte"
import { timeout } from "../../__tests__/util"

describe("Svelte integration", () => {
test("inital render", async () => {
const { getByTestId } = render(Comp)

expect(getByTestId("name")).toHaveTextContent("Initializing")
})
expect(getByTestId("didWorld")).toHaveTextContent("false")

await act(async () => {
await timeout(5000)
})

expect(getByTestId("name")).toHaveTextContent("Ready")
expect(getByTestId("didWorld")).toHaveTextContent("true")
}, 6000)
})
27 changes: 25 additions & 2 deletions src/svelte/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { BoundStateFn, StateTransition } from "../state.js"
import { Context, createInitialContext } from "../context.js"
import { type Readable, readable } from "svelte/store"
import { Runtime, createRuntime } from "../runtime.js"
import { onMount } from "svelte"

export interface ContextValue<
SM extends { [key: string]: BoundStateFn<any, any, any> },
Expand Down Expand Up @@ -30,7 +31,16 @@ export const createStore = <
SM extends { [key: string]: BoundStateFn<any, any, any> },
AM extends { [key: string]: (...args: Array<any>) => Action<any, any> },
OAM extends { [key: string]: (...args: Array<any>) => Action<any, any> },
R extends Readable<ContextValue<SM, AM, OAM>>,
R extends Readable<ContextValue<SM, AM, OAM>> & {
respondOnMount: <
T extends OAM["type"],
P extends Extract<OAM, { type: T }>["payload"],
A extends ReturnType<AM[keyof AM]>,
>(
type: T,
handler: (payload: P) => Promise<A> | A | void,
) => void
},
>(
_states: SM,
actions: AM,
Expand All @@ -56,7 +66,7 @@ export const createStore = <
runtime,
}

return readable(initialContext, set => {
const store = readable(initialContext, set => {
const unsub = runtime.onContextChange(context =>
set({
context,
Expand All @@ -71,4 +81,17 @@ export const createStore = <

return unsub
}) as R

store.respondOnMount = <
T extends OAM["type"],
P extends Extract<OAM, { type: T }>["payload"],
A extends ReturnType<AM[keyof AM]>,
>(
type: T,
handler: (payload: P) => Promise<A> | A | void,
) => {
onMount(() => runtime.respondToOutput(type, handler))
}

return store
}

0 comments on commit c1ba485

Please sign in to comment.