|
| 1 | +import { Context, Effect, Layer, Schema } from "effect" |
| 2 | +import { Atom } from "effect/unstable/reactivity" |
| 3 | +import { Machine } from "@typeonce/effect-machine" |
| 4 | +import { AtomMachine } from "@typeonce/effect-machine/reactivity" |
| 5 | + |
| 6 | +type Equal<Left, Right> = |
| 7 | + (<Type>() => Type extends Left ? 1 : 2) extends <Type>() => Type extends Right ? 1 : 2 ? true : false |
| 8 | +type Expect<Type extends true> = Type |
| 9 | + |
| 10 | +class ExternalService extends Context.Service<ExternalService, string>()("consumer/ExternalService") {} |
| 11 | +class InitialService extends Context.Service<InitialService, string>()("consumer/InitialService") {} |
| 12 | + |
| 13 | +class InitialFailure { |
| 14 | + readonly _tag = "InitialFailure" |
| 15 | +} |
| 16 | +class TransitionFailure { |
| 17 | + readonly _tag = "TransitionFailure" |
| 18 | +} |
| 19 | +class ActionFailure { |
| 20 | + readonly _tag = "ActionFailure" |
| 21 | +} |
| 22 | +class RuntimeFailure { |
| 23 | + readonly _tag = "RuntimeFailure" |
| 24 | +} |
| 25 | + |
| 26 | +const State = Schema.TaggedUnion({ |
| 27 | + Idle: {}, |
| 28 | + Ready: {}, |
| 29 | + Editor: {}, |
| 30 | + Editing: { value: Schema.String }, |
| 31 | + Saving: { value: Schema.String }, |
| 32 | + Done: { value: Schema.String } |
| 33 | +}) |
| 34 | +const Event = Schema.TaggedUnion({ |
| 35 | + Begin: {}, |
| 36 | + Save: { value: Schema.String } |
| 37 | +}) |
| 38 | +const Internal = Schema.TaggedUnion({ |
| 39 | + Loaded: { value: Schema.String }, |
| 40 | + ChildCompleted: { value: Schema.String }, |
| 41 | + ChildNotice: { value: Schema.String } |
| 42 | +}) |
| 43 | +const Emitted = Schema.TaggedUnion({ |
| 44 | + Notice: { value: Schema.String } |
| 45 | +}) |
| 46 | +const ChildState = Schema.TaggedUnion({ |
| 47 | + Done: { value: Schema.String } |
| 48 | +}) |
| 49 | + |
| 50 | +const ChildStates = Machine.defineStates({ |
| 51 | + Done: { |
| 52 | + schema: ChildState.cases.Done, |
| 53 | + type: "final", |
| 54 | + output: Schema.String |
| 55 | + } |
| 56 | +}) |
| 57 | +const childMachine = Machine.make({ |
| 58 | + states: ChildStates.states, |
| 59 | + events: [], |
| 60 | + emits: [Internal.cases.ChildNotice], |
| 61 | + input: Schema.Struct({ value: Schema.String }), |
| 62 | + initial: ({ value }) => ChildStates.initial.Done(ChildState.cases.Done.make({ value })) |
| 63 | +}).handle({ |
| 64 | + Done: { |
| 65 | + entry: ({ state }) => |
| 66 | + Machine.action( |
| 67 | + Effect.gen(function* () { |
| 68 | + const runtime = yield* Machine.runtime<{ readonly emits: typeof Internal.cases.ChildNotice.Type }>() |
| 69 | + yield* runtime.sendParent(Internal.cases.ChildNotice.make({ value: state.value })) |
| 70 | + }) |
| 71 | + ), |
| 72 | + output: ({ state }) => state.value |
| 73 | + } |
| 74 | +}) |
| 75 | +const Child = Machine.child("child", childMachine) |
| 76 | + |
| 77 | +const States = Machine.defineStates({ |
| 78 | + Idle: State.cases.Idle, |
| 79 | + Ready: { |
| 80 | + schema: State.cases.Ready, |
| 81 | + initial: "Editor", |
| 82 | + states: { |
| 83 | + Editor: { |
| 84 | + schema: State.cases.Editor, |
| 85 | + initial: "Editing", |
| 86 | + states: { |
| 87 | + Editing: State.cases.Editing, |
| 88 | + Saving: State.cases.Saving |
| 89 | + } |
| 90 | + } |
| 91 | + } |
| 92 | + }, |
| 93 | + Done: { |
| 94 | + schema: State.cases.Done, |
| 95 | + type: "final", |
| 96 | + output: Schema.String |
| 97 | + } |
| 98 | +}) |
| 99 | + |
| 100 | +const machine = Machine.make({ |
| 101 | + states: States.states, |
| 102 | + events: [Event.cases.Begin, Event.cases.Save], |
| 103 | + internalEvents: [Internal.cases.Loaded, Internal.cases.ChildCompleted, ...childMachine.emits], |
| 104 | + emits: [Emitted.cases.Notice], |
| 105 | + input: Schema.Struct({ seed: Schema.String }), |
| 106 | + initial: ({ seed }) => |
| 107 | + Effect.gen(function* () { |
| 108 | + yield* InitialService |
| 109 | + if (seed.length < 0) return yield* Effect.fail(new InitialFailure()) |
| 110 | + return States.initial.Idle(State.cases.Idle.make({})) |
| 111 | + }) |
| 112 | +}).handle({ |
| 113 | + Idle: { |
| 114 | + invoke: Machine.invoke({ |
| 115 | + id: "deep-inline-invoke", |
| 116 | + src: () => Machine.effect(Effect.as(ExternalService, Internal.cases.Loaded.make({ value: "loaded" }))) |
| 117 | + }), |
| 118 | + on: { |
| 119 | + Begin: ({ target }) => |
| 120 | + target.full.Ready(State.cases.Ready.make({}), (ready) => |
| 121 | + ready.Editor(State.cases.Editor.make({}), (editor) => |
| 122 | + editor.Editing(State.cases.Editing.make({ value: "ready" })) |
| 123 | + ) |
| 124 | + ) |
| 125 | + } |
| 126 | + }, |
| 127 | + Ready: { |
| 128 | + states: { |
| 129 | + Editor: { |
| 130 | + states: { |
| 131 | + Editing: { |
| 132 | + on: { |
| 133 | + Save: ({ event, target }) => |
| 134 | + Machine.action( |
| 135 | + Effect.gen(function* () { |
| 136 | + yield* ExternalService |
| 137 | + return yield* Effect.fail(new ActionFailure()) |
| 138 | + }), |
| 139 | + target.local.Saving(State.cases.Saving.make({ value: event.value })) |
| 140 | + ), |
| 141 | + Loaded: () => Effect.fail(new TransitionFailure()) |
| 142 | + } |
| 143 | + }, |
| 144 | + Saving: { |
| 145 | + invoke: ({ state }) => |
| 146 | + Machine.invokeMachine({ |
| 147 | + child: Child, |
| 148 | + input: { value: state.value }, |
| 149 | + onDone: ({ output }) => Internal.cases.ChildCompleted.make({ value: output }) |
| 150 | + }), |
| 151 | + on: { |
| 152 | + ChildNotice: ({ event, target }) => |
| 153 | + Machine.action( |
| 154 | + Effect.gen(function* () { |
| 155 | + const runtime = yield* Machine.runtime<{ readonly emits: typeof Emitted.cases.Notice.Type }>() |
| 156 | + yield* runtime.sendParent(Emitted.cases.Notice.make({ value: event.value })) |
| 157 | + }), |
| 158 | + target.local.Saving(State.cases.Saving.make({ value: event.value })) |
| 159 | + ), |
| 160 | + ChildCompleted: ({ event, target }) => target.full.Done(State.cases.Done.make({ value: event.value })) |
| 161 | + } |
| 162 | + } |
| 163 | + } |
| 164 | + } |
| 165 | + } |
| 166 | + }, |
| 167 | + Done: { |
| 168 | + output: ({ state }) => state.value |
| 169 | + } |
| 170 | +}) |
| 171 | + |
| 172 | +const runtime = Atom.runtime( |
| 173 | + Layer.mergeAll( |
| 174 | + Layer.succeed(ExternalService, "provided"), |
| 175 | + Layer.succeed(InitialService, "provided"), |
| 176 | + Layer.effectDiscard(Effect.fail(new RuntimeFailure())) |
| 177 | + ) |
| 178 | +) |
| 179 | +const Bound = AtomMachine.bind(runtime) |
| 180 | +const machineAtom = Bound.make(machine, { seed: "initial" }) |
| 181 | + |
| 182 | +type Snapshot = Machine.Machine.Snapshot<typeof States.states> |
| 183 | +type StateSuccess = Atom.Success<typeof machineAtom.state> |
| 184 | +type SendEvent = typeof machineAtom.send extends Atom.Writable<any, infer InputEvent> ? InputEvent : never |
| 185 | +type Output = typeof machineAtom extends AtomMachine.MachineAtom<any, any, any, infer Value, any> ? Value : never |
| 186 | +type Failure = Atom.Failure<typeof machineAtom.result> |
| 187 | + |
| 188 | +type StateIsExact = Expect<Equal<StateSuccess, Snapshot>> |
| 189 | +type EventsArePublicOnly = Expect<Equal<SendEvent, typeof Event.Type>> |
| 190 | +type OutputIsExact = Expect<Equal<Output, string>> |
| 191 | +type InitialErrorIsPreserved = Expect<Equal<Extract<Failure, InitialFailure>, InitialFailure>> |
| 192 | +type TransitionErrorIsPreserved = Expect<Equal<Extract<Failure, TransitionFailure>, TransitionFailure>> |
| 193 | +type ActionErrorIsPreserved = Expect<Equal<Extract<Failure, ActionFailure>, ActionFailure>> |
| 194 | +type RuntimeErrorIsPreserved = Expect<Equal<Extract<Failure, RuntimeFailure>, RuntimeFailure>> |
| 195 | +type FailureIsNotUnknown = Expect<Equal<unknown extends Failure ? true : false, false>> |
| 196 | +type MachineServicesAreNotAny = Expect< |
| 197 | + Equal<0 extends 1 & Machine.Machine.Services<typeof machine> ? true : false, false> |
| 198 | +> |
| 199 | + |
| 200 | +// @ts-expect-error Input is required. |
| 201 | +Bound.make(machine) |
| 202 | +// @ts-expect-error Input retains its exact decoded type. |
| 203 | +Bound.make(machine, { seed: 1 }) |
| 204 | +// @ts-expect-error The bound runtime must provide every external service. |
| 205 | +AtomMachine.bind(Atom.runtime(Layer.empty)).make(machine, { seed: "initial" }) |
| 206 | +const erased: Machine.Machine.Any = machine |
| 207 | +// @ts-expect-error Machine.Any erasure cannot manufacture concrete protocol or output proof. |
| 208 | +Bound.make(erased, { seed: "initial" }) |
| 209 | + |
| 210 | +void machineAtom |
| 211 | +export type { |
| 212 | + ActionErrorIsPreserved, |
| 213 | + EventsArePublicOnly, |
| 214 | + FailureIsNotUnknown, |
| 215 | + InitialErrorIsPreserved, |
| 216 | + MachineServicesAreNotAny, |
| 217 | + OutputIsExact, |
| 218 | + RuntimeErrorIsPreserved, |
| 219 | + StateIsExact, |
| 220 | + TransitionErrorIsPreserved |
| 221 | +} |
0 commit comments