Skip to content

Commit

Permalink
feat: improve messages
Browse files Browse the repository at this point in the history
  • Loading branch information
prncss-xyz committed Nov 2, 2024
1 parent ba4d4a1 commit b8abb49
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 63 deletions.
4 changes: 2 additions & 2 deletions packages/demos/jotai-turnstile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"@constellar/core": "*",
"@constellar/jotai": "*",
"@constellar/core": "workspace:^",
"@constellar/jotai": "workspace:^",
"@vitejs/plugin-react": "^4.3.1",
"jotai": "^2.9.3",
"jotai-effect": "^1.0.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/demos/jotai-turnstile/src/machine.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { multiStateMachine } from '@constellar/core'

type State =
export type State =
| { effects: { payment: { id: string; now: number } }; type: 'payment' }
| { id: string; type: 'payment' }
| { type: 'locked' }
Expand Down
6 changes: 5 additions & 1 deletion packages/libs/core/src/machines/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface IMachine<Event, State, Message, Transformed, SubState, Final> {
reducer: (
event: Event,
transformed: Transformed,
send: (e: Message) => void,
emit: (e: Message) => void,
) => State | undefined
transform: (state: State) => Transformed
visit: <Acc>(
Expand Down Expand Up @@ -35,3 +35,7 @@ export function fromSendable<Event extends Typed>(
({ type: event } as unknown as Event)
: event
}

export function withSend<Event extends Typed>(emit: (event: Event) => void) {
return (e: Sendable<Event>) => emit(fromSendable(e)) as undefined
}
4 changes: 2 additions & 2 deletions packages/libs/core/src/machines/effects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ test('effects without interpreter', () => {
const cb = machineCb(m)
const { visit } = cb(m.transform({ n: 0 }))
m.transform({ n: 1 })
const eff = new MachineEffects(() => {}, undefined)
eff.update(visit)
const eff = new MachineEffects(undefined)
eff.update(visit, () => {})
}).not.toThrowError()
})
46 changes: 23 additions & 23 deletions packages/libs/core/src/machines/effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,30 +29,29 @@ export class MachineEffects<Event, SubState> {
string,
Map<string, { args: any; unmount: (() => void) | void }>
>()
constructor(
private send: (event: Event) => void,
private interpreter: Interpreter<Event, SubState>,
) {}
private foldSubState(subState: SubState, acc: Set<string>, index: string) {
if (this.interpreter === undefined) return acc
acc.add(index)
for (const entry of Object.entries(this.interpreter)) {
const [effect, cb] = entry as [string, any]
const args = ((subState as any).effects as any)?.[effect]
let fromIndex = this.last.get(index)
if (!fromIndex) {
fromIndex = new Map()
this.last.set(index, fromIndex)
constructor(private interpreter: Interpreter<Event, SubState>) {}
private foldSubState(send: (event: Event) => void) {
return (subState: SubState, acc: Set<string>, index: string) => {
if (this.interpreter === undefined) return acc
acc.add(index)
for (const entry of Object.entries(this.interpreter)) {
const [effect, cb] = entry as [string, any]
const args = ((subState as any).effects as any)?.[effect]
let fromIndex = this.last.get(index)
if (!fromIndex) {
fromIndex = new Map()
this.last.set(index, fromIndex)
}
const last = fromIndex.get(effect)
if (shallowEqual(last?.args, args)) continue
last?.unmount?.()
fromIndex.set(effect, {
args,
unmount: args === undefined ? undefined : cb(args, send),
})
}
const last = fromIndex.get(effect)
if (shallowEqual(last?.args, args)) continue
last?.unmount?.()
fromIndex.set(effect, {
args,
unmount: args === undefined ? undefined : cb(args, this.send),
})
return acc
}
return acc
}
flush(indices?: Set<string>) {
this.last.forEach((fromIndex, index) => {
Expand All @@ -66,7 +65,8 @@ export class MachineEffects<Event, SubState> {
f: (subState: SubState, acc: Set<string>, index: string) => Set<string>,
acc: Set<string>,
) => Set<string>,
send: (event: Event) => void,
) {
this.flush(visit(this.foldSubState.bind(this), new Set<string>()))
this.flush(visit(this.foldSubState(send), new Set<string>()))
}
}
5 changes: 3 additions & 2 deletions packages/libs/core/src/machines/listener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ describe('listener', () => {
expect(b).toHaveBeenCalledTimes(0)
})
test('function', () => {
const listener = toListener(vi.fn())
const a = vi.fn()
const listener = toListener(a)
listener({ type: 'a', value: 1 }, 2)
expect(listener).toHaveBeenCalledWith({ type: 'a', value: 1 }, 2)
expect(a).toHaveBeenCalledWith({ type: 'a', value: 1 }, 2)
})
})
14 changes: 9 additions & 5 deletions packages/libs/core/src/machines/listener.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { isFunction, Typed } from '../utils'
import { fromSendable, Sendable } from './core'

export type Listener<Message extends Typed, Args extends any[]> =
| ((event: Message, ...args: Args) => void)
| ((message: Message, ...args: Args) => void)
| {
[K in Message['type']]: (
event: { type: K } & Message,
message: { type: K } & Message,
...args: Args
) => void
}

export function toListener<Message extends Typed, Args extends any[]>(
listener: Listener<Message, Args>,
): (event: Message, ...args: Args) => void {
if (isFunction(listener)) return listener
): (message: Sendable<Message>, ...args: Args) => void {
if (isFunction(listener))
return (message: Sendable<Message>, ...args) =>
listener(fromSendable(message), ...args)
return (message, ...args) => {
;(listener as any)[message.type](message, ...args)
const m = fromSendable(message)
;(listener as any)[m.type](m, ...args)
}
}
9 changes: 2 additions & 7 deletions packages/libs/core/src/machines/multi-state.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { fromSendable, IMachine, Sendable } from '.'
import { fromSendable, IMachine, Sendable, withSend } from '.'
import { Init, isEmpty, isFunction, Prettify, toInit, Typed } from '../utils'

type ArgOfInit<T> = T extends (p: infer R) => any ? R : void
Expand All @@ -25,7 +25,7 @@ type AnyStates<
>(
event: E,
state: S,
send: (message: Sendable<Message>) => undefined,
emit: (message: Sendable<Message>) => undefined,
) => Sendable<State> | undefined)
| Sendable<State>
}>
Expand Down Expand Up @@ -88,11 +88,6 @@ type Final<
States extends Record<string, unknown>,
> = FinalStates<States> & State

function withSend<Message extends Typed>(emit: (message: Message) => void) {
return (message: Sendable<Message>) =>
emit(fromSendable(message)) as undefined
}

export function multiStateMachine<
Event extends Typed,
State extends Typed,
Expand Down
6 changes: 3 additions & 3 deletions packages/libs/core/src/machines/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ class ObjectMachine<Event, State, Message, Transformed, SubState, Final> {
this.state = machine.transform(machine.init)
this.final = machine.getFinal(this.state)
if (opts?.interpreter) {
this.effects = new MachineEffects(this.send.bind(this), opts.interpreter)
this.effects.update(this.visit.bind(this))
this.effects = new MachineEffects(opts.interpreter)
this.effects.update(this.visit.bind(this), this.send.bind(this))
}
}
private onChange() {
if (!this.effects) return
const run = this.queue.length === 0
this.effects.update(this.visit.bind(this))
this.effects.update(this.visit.bind(this), this.send.bind(this))
if (!run) return
while (true) {
const event = this.queue.shift()
Expand Down
33 changes: 32 additions & 1 deletion packages/libs/jotai/src/machine.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { multiStateMachine, simpleStateMachine } from '@constellar/core'
import {
Listener,
multiStateMachine,
simpleStateMachine,
} from '@constellar/core'
import { fireEvent, render, screen } from '@testing-library/react'
import { atom, createStore, useAtom } from 'jotai'
import { useState } from 'react'
Expand Down Expand Up @@ -248,3 +252,30 @@ describe('messages', () => {
expect(store.get(countAtom)).toBe(1)
})
})

/*
describe('listener', () => {
type Message = { p: number; type: 'out' } | { q: string; type: 'in' }
const x: Listener<Message, [number, string]> = {
in: ({ q }, x, y) => console.log(e.type, x, q),
out: ({ p }, x, y) => console.log(e.type, x, p),
}
type State = { type: 'a' }
type Event = { q: string; type: 'a' }
const someMachine = multiStateMachine<
Event,
State,
object,
object,
Message
>()({
init: 'a',
states: {
a: {},
},
})
const someAtom = machineAtom(someMachine(), {
listener: (x) => console.log(x),
})
})
*/
31 changes: 25 additions & 6 deletions packages/libs/jotai/src/machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function machineAtom<
atomFactory: (
init: State,
) => WritableAtom<Promise<State>, [Promise<State>], R>
interpreter?: Interpreter<Event, SubState>
listener?: Listener<Message, [Getter, Setter]>
},
): WritableAtom<
Expand All @@ -63,6 +64,7 @@ export function machineAtom<
>,
opts?: {
atomFactory?: (init: State) => WritableAtom<State, [State], R>
interpreter?: Interpreter<Event, SubState>
listener?: Listener<Message, [Getter, Setter]>
},
): WritableAtom<
Expand All @@ -89,6 +91,7 @@ export function machineAtom<
>,
opts?: {
atomFactory?: (init: State) => WritableAtom<State, [State], R>
interpreter?: Interpreter<Event, SubState>
listener?: Listener<Message, [Getter, Setter]>
},
) {
Expand All @@ -98,7 +101,16 @@ export function machineAtom<
const reducer = machine.reducer
const cb = machineCb(machine)
const listener = opts?.listener ? toListener(opts.listener) : () => {}
return atom(
const effAtom = opts?.interpreter
? atom<MachineEffects<Event, SubState>>()
: null
if (effAtom)
effAtom.onMount = (setAtom) => {
const effects = new MachineEffects<Event, SubState>(opts!.interpreter!)
setAtom(effects)
return () => effects.flush()
}
const resAtom = atom(
(get) => unwrap(get(stateAtom), cb),
(get, set, event: Sendable<Event>) =>
unwrap(get(stateAtom), (state) => {
Expand All @@ -107,8 +119,18 @@ export function machineAtom<
)
if (nextState === undefined) return
set(stateAtom, nextState)
const send = (e: Event) => set(resAtom, e)
if (effAtom)
setTimeout(
() =>
unwrap(get(resAtom), (res) =>
get(effAtom)!.update(res.visit, send),
),
0,
)
}),
)
return resAtom
}

export function useMachineEffects<
Expand All @@ -122,14 +144,11 @@ export function useMachineEffects<
) {
const machineEffects = useRef<MachineEffects<Event, Transformed>>()
useEffect(() => {
machineEffects.current = new MachineEffects<Event, Transformed>(
send,
interpreter,
)
machineEffects.current = new MachineEffects<Event, Transformed>(interpreter)
return () => machineEffects.current!.flush()
}, [interpreter, send])
useEffect(
() => machineEffects.current!.update(transformed.visit),
() => machineEffects.current!.update(transformed.visit, send),
[transformed, send, interpreter],
)
}
Expand Down
20 changes: 10 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit b8abb49

Please sign in to comment.