@kestack/kabuki 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +274 -2
- package/dist/core/actor/actor.d.ts +42 -0
- package/dist/core/actor/actor.d.ts.map +1 -0
- package/dist/core/actor/actor.js +168 -0
- package/dist/core/actor/command.d.ts +24 -0
- package/dist/core/actor/command.d.ts.map +1 -0
- package/dist/core/actor/command.js +31 -0
- package/dist/core/actor/index.d.ts +6 -0
- package/dist/core/actor/index.d.ts.map +1 -0
- package/dist/core/actor/index.js +3 -0
- package/dist/core/actor/types.d.ts +295 -0
- package/dist/core/actor/types.d.ts.map +1 -0
- package/dist/core/actor/types.js +2 -0
- package/dist/core/history/history.d.ts +67 -0
- package/dist/core/history/history.d.ts.map +1 -0
- package/dist/core/history/history.js +145 -0
- package/dist/core/history/index.d.ts +5 -0
- package/dist/core/history/index.d.ts.map +1 -0
- package/dist/core/history/index.js +2 -0
- package/dist/core/history/replay.d.ts +34 -0
- package/dist/core/history/replay.d.ts.map +1 -0
- package/dist/core/history/replay.js +82 -0
- package/dist/core/history/saved.d.ts +7 -0
- package/dist/core/history/saved.d.ts.map +1 -0
- package/dist/core/history/saved.js +126 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +4 -0
- package/dist/core/runtime/index.d.ts +4 -0
- package/dist/core/runtime/index.d.ts.map +1 -0
- package/dist/core/runtime/index.js +1 -0
- package/dist/core/runtime/instance/command.d.ts +4 -0
- package/dist/core/runtime/instance/command.d.ts.map +1 -0
- package/dist/core/runtime/instance/command.js +12 -0
- package/dist/core/runtime/instance/index.d.ts +4 -0
- package/dist/core/runtime/instance/index.d.ts.map +1 -0
- package/dist/core/runtime/instance/index.js +1 -0
- package/dist/core/runtime/instance/instance.d.ts +16 -0
- package/dist/core/runtime/instance/instance.d.ts.map +1 -0
- package/dist/core/runtime/instance/instance.js +113 -0
- package/dist/core/runtime/instance/self.d.ts +35 -0
- package/dist/core/runtime/instance/self.d.ts.map +1 -0
- package/dist/core/runtime/instance/self.js +42 -0
- package/dist/core/runtime/instance/worker.d.ts +16 -0
- package/dist/core/runtime/instance/worker.d.ts.map +1 -0
- package/dist/core/runtime/instance/worker.js +52 -0
- package/dist/core/runtime/mailbox.d.ts +10 -0
- package/dist/core/runtime/mailbox.d.ts.map +1 -0
- package/dist/core/runtime/mailbox.js +64 -0
- package/dist/core/runtime/runtime.d.ts +36 -0
- package/dist/core/runtime/runtime.d.ts.map +1 -0
- package/dist/core/runtime/runtime.js +93 -0
- package/dist/core/runtime/time.d.ts +11 -0
- package/dist/core/runtime/time.d.ts.map +1 -0
- package/dist/core/runtime/time.js +22 -0
- package/dist/core/snapshot/handle.d.ts +14 -0
- package/dist/core/snapshot/handle.d.ts.map +1 -0
- package/dist/core/snapshot/handle.js +70 -0
- package/dist/core/snapshot/index.d.ts +8 -0
- package/dist/core/snapshot/index.d.ts.map +1 -0
- package/dist/core/snapshot/index.js +4 -0
- package/dist/core/snapshot/invoke.d.ts +17 -0
- package/dist/core/snapshot/invoke.d.ts.map +1 -0
- package/dist/core/snapshot/invoke.js +119 -0
- package/dist/core/snapshot/restore.d.ts +16 -0
- package/dist/core/snapshot/restore.d.ts.map +1 -0
- package/dist/core/snapshot/restore.js +63 -0
- package/dist/core/snapshot/snapshot.d.ts +61 -0
- package/dist/core/snapshot/snapshot.d.ts.map +1 -0
- package/dist/core/snapshot/snapshot.js +4 -0
- package/dist/core/snapshot/tree.d.ts +26 -0
- package/dist/core/snapshot/tree.d.ts.map +1 -0
- package/dist/core/snapshot/tree.js +77 -0
- package/dist/react/app.d.ts +49 -0
- package/dist/react/app.d.ts.map +1 -0
- package/dist/react/app.js +214 -0
- package/dist/react/child.d.ts +32 -0
- package/dist/react/child.d.ts.map +1 -0
- package/dist/react/child.js +80 -0
- package/dist/react/drawn.d.ts +4 -0
- package/dist/react/drawn.d.ts.map +1 -0
- package/dist/react/drawn.js +118 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/registry.d.ts +12 -0
- package/dist/react/registry.d.ts.map +1 -0
- package/dist/react/registry.js +25 -0
- package/dist/react/view.d.ts +80 -0
- package/dist/react/view.d.ts.map +1 -0
- package/dist/react/view.js +47 -0
- package/package.json +55 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kishore Polamarasetty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,275 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @kestack/kabuki
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An app as one tree of Actors outside React: each Actor a small state machine driven by Messages, one immutable Snapshot for the whole app, Effect running the work, and React only drawing it.
|
|
4
|
+
|
|
5
|
+
## Big picture
|
|
6
|
+
|
|
7
|
+
Kabuki is the Elm architecture, as [Foldkit](https://github.com/foldkit/foldkit)
|
|
8
|
+
builds it on Effect, organized as a tree of Actors and drawn by React. Each
|
|
9
|
+
Actor is a state machine and an actor at once: Messages are the only way it
|
|
10
|
+
changes, and its State decides which Children it Invokes, which Capabilities
|
|
11
|
+
it Provides to them and which Workers run. Leaving a State stops all of it.
|
|
12
|
+
Any fact in the Model can own a Worker too: an upload runs for as long as its
|
|
13
|
+
row says Uploading.
|
|
14
|
+
Keyed Children follow the Model: a list of items can be a list of Actors.
|
|
15
|
+
It is declarative, as React is: a parent says what should be, never what to
|
|
16
|
+
do. It controls its Children only by whether they exist and by their Props,
|
|
17
|
+
and what it must control it owns, as React lifts state up.
|
|
18
|
+
|
|
19
|
+
The whole app is one immutable Snapshot, and handling a Message is a pure
|
|
20
|
+
function from one Snapshot to the next. There is one mailbox, and a Message is
|
|
21
|
+
handled before `send` returns. Around that pure core, everything that touches
|
|
22
|
+
the outside world is Effect: each Instance's work lives in nested Scopes, and
|
|
23
|
+
only the work where a Message landed is stopped or started. An Actor's own
|
|
24
|
+
code reaches only the Capabilities it Requires. What its Children need bubbles
|
|
25
|
+
up until an ancestor Provides it, and the app does not compile until its Layer
|
|
26
|
+
covers the rest.
|
|
27
|
+
|
|
28
|
+
The Runtime runs a History. It starts the root Instance at the History's
|
|
29
|
+
Head, hands it every Message through one Mailbox, and writes each one into
|
|
30
|
+
the History; every Instance runs the rest of its own life, starting and
|
|
31
|
+
stopping its States, Children, Workers and Commands. The History is a tree
|
|
32
|
+
of Messages, like git commits, that replays itself to the Snapshot right
|
|
33
|
+
after any one of them, on any Branch, while the live app keeps running. Stop,
|
|
34
|
+
move its Head, and start again, and new Messages grow a new Branch. A History
|
|
35
|
+
is also what an app is saved as and drawn from before it runs: on a server,
|
|
36
|
+
at build time, and across a hot reload.
|
|
37
|
+
|
|
38
|
+
Read the language in [CONTEXT.md](./CONTEXT.md), the decisions in
|
|
39
|
+
[docs/adr/](./docs/adr/), and how the History works in
|
|
40
|
+
[docs/history.md](./docs/history.md). A live demo, Opening an app, is at `/demos/opening-an-app` in the docs app: every concept in one story, drawn as the tree.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
pnpm add @kestack/kabuki effect react
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- `effect`: Schemas describe Inputs, Models, States and Messages, and every
|
|
49
|
+
one is checked against its Schema as it is made; Workers, Commands and
|
|
50
|
+
Capabilities are Effect values and Layers.
|
|
51
|
+
- `react`: `@kestack/kabuki/react` draws the tree with React components.
|
|
52
|
+
|
|
53
|
+
## Exports
|
|
54
|
+
|
|
55
|
+
### `@kestack/kabuki`
|
|
56
|
+
|
|
57
|
+
| Export | What it does |
|
|
58
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
59
|
+
| `Actor.make` | Defines an Actor from its Requires and Schemas. `.build` adds how it behaves, at the top and in one block per State. |
|
|
60
|
+
| `Actor.many` | Declares a keyed Child slot: one Instance of an Actor per key the parent Invokes. |
|
|
61
|
+
| `Command.make` | Defines a Command from its name, the Schema of its args and the Effect it runs; an Update calls it with the args. |
|
|
62
|
+
| `Runtime.start` | Runs an Actor, or carries a History on, inside a Scope, with the app's Layer; gives back `send`, `start` and `stop`. |
|
|
63
|
+
| `History.make` | A new, empty History for an Actor, with the root's Input. |
|
|
64
|
+
| `History.load` | A History carrying on from one saved before, read with the Actor's code as it is now. |
|
|
65
|
+
| `History.restored` | The lightest History an app can be saved as: one Restore of a Snapshot at a Time, right after init. |
|
|
66
|
+
| `Replay.make` | Plays Branches of one app from init, remembering Snapshots so playing again starts from the nearest one. |
|
|
67
|
+
| `Replay.fold` | The Snapshot right after a Branch, played now from init with the root's Input, with no yielding. |
|
|
68
|
+
| `restore` | The Restore Message: given to the root, it replaces the whole Snapshot with a saved one, checked against the Actors. |
|
|
69
|
+
| `isRestore` | Tells a Restore from any other Message. |
|
|
70
|
+
| `RESTORE` | The Restore Message's tag. |
|
|
71
|
+
| `init` | The Snapshot right after init: the root and every Child its State Invokes. |
|
|
72
|
+
| `handle` | Applies one Envelope to a Snapshot and gives the next one, purely. |
|
|
73
|
+
| `outcomeOf` | Works out what came of one Envelope on a Snapshot: handled, ignored or dropped. The History never keeps it. |
|
|
74
|
+
| `instanceAt` | Finds the Instance with an ID in a Snapshot. |
|
|
75
|
+
| `isMany` | Tells a keyed Child slot from a fixed one in an Instance's Children. |
|
|
76
|
+
|
|
77
|
+
### `@kestack/kabuki/react`
|
|
78
|
+
|
|
79
|
+
| Export | What it does |
|
|
80
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| `View.make` | Defines how one Actor is drawn, one function for every State, as a React component that takes the Instance, and a `ref` and `style`, from its parent. |
|
|
82
|
+
| `toReact` | Turns a root Actor, its View and a Layer into one React component that runs the whole app, one Runtime for every mount; `useRuntime` reads its History, shows any Step, and stops and starts it. Drawn before it runs, on a server too, from a saved History if given one. |
|
|
83
|
+
| `useChild` | Reads one fixed Child's Model, State and Props from its parent's View, to draw, or only what an optional selector picks; an escape hatch that can never Send. |
|
|
84
|
+
| `useChildren` | Reads a slot of keyed Children the same way, in order; only the component that calls it re-renders, as they change or as what the selector picks does. |
|
|
85
|
+
|
|
86
|
+
## Usage
|
|
87
|
+
|
|
88
|
+
### A gate whose States decide what exists
|
|
89
|
+
|
|
90
|
+
This root signs a user in. While `Anonymous` it Provides `SignIn` and Invokes
|
|
91
|
+
`login`; while `Authenticated` it Provides `Session` and Invokes `todos`.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
export const Auth = Actor.make('Auth', {
|
|
95
|
+
requires: [Server],
|
|
96
|
+
state: Schema.TaggedUnion({
|
|
97
|
+
Checking: {},
|
|
98
|
+
Anonymous: {},
|
|
99
|
+
Authenticated: { user: Schema.String },
|
|
100
|
+
}),
|
|
101
|
+
message: Schema.TaggedUnion({
|
|
102
|
+
CheckedSession: { user: Schema.optional(Schema.String) },
|
|
103
|
+
LoggedIn: { user: Schema.String },
|
|
104
|
+
LoggedOut: {},
|
|
105
|
+
}),
|
|
106
|
+
}).build({
|
|
107
|
+
init: () => ({ state: { _tag: 'Checking' } }),
|
|
108
|
+
state: {
|
|
109
|
+
Checking: {
|
|
110
|
+
workers: {
|
|
111
|
+
check: {
|
|
112
|
+
run: (self) =>
|
|
113
|
+
Effect.gen(function* () {
|
|
114
|
+
const user = yield* (yield* Server).checkSession;
|
|
115
|
+
yield* self.send({ _tag: 'CheckedSession', user });
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
on: {
|
|
120
|
+
CheckedSession: ({ user }) => ({
|
|
121
|
+
state: user ? { _tag: 'Authenticated', user } : { _tag: 'Anonymous' },
|
|
122
|
+
}),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
Anonymous: {
|
|
126
|
+
children: { login: Login },
|
|
127
|
+
provides: (self) =>
|
|
128
|
+
Layer.succeed(SignIn, {
|
|
129
|
+
complete: (user) => self.send({ _tag: 'LoggedIn', user }),
|
|
130
|
+
}),
|
|
131
|
+
on: {
|
|
132
|
+
LoggedIn: ({ user }) => ({ state: { _tag: 'Authenticated', user } }),
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
Authenticated: {
|
|
136
|
+
children: { todos: Todos },
|
|
137
|
+
provides: (self) =>
|
|
138
|
+
Layer.succeed(Session, {
|
|
139
|
+
user: self.get.pipe(Effect.map(({ state }) => state.user)),
|
|
140
|
+
logOut: self.send({ _tag: 'LoggedOut' }),
|
|
141
|
+
}),
|
|
142
|
+
on: { LoggedOut: () => ({ state: { _tag: 'Anonymous' } }) },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- `make` takes only the Requires and the Schemas, which fix every type. `build` is checked against them: an Update that returns a State the Schema does not have, or a `state.user` read where there is none, does not compile.
|
|
149
|
+
- `build` has two Levels. What is written at the top holds for as long as the Instance lives; what is written in a State's block holds while that State lasts. Each Level can hold `provides`, `children`, `invoke`, `on` and `workers`.
|
|
150
|
+
- An Update that returns a different `_tag` is a Transition: the old State's Children, Workers and Capabilities stop, the new State's start. A Child written at the top lasts through it.
|
|
151
|
+
- A Worker is a named, scoped Effect given `self`: `send` to its own Instance, `get` and `changes` for its current data, and `now` for the app's Time. Written in the `Checking` block, `check` runs while that State lasts.
|
|
152
|
+
- Provides is one Layer per Level, built each time its Level starts. A Capability that reads the Model reads it through `self`, so it never goes stale. It cancels out what the Children below it need; a need nothing cancels bubbles up to the app's Layer.
|
|
153
|
+
- Requires caps the Actor's own code: a Worker, Command or Provide that reaches for anything else does not compile. Update and init never see Capabilities.
|
|
154
|
+
|
|
155
|
+
### A list of Actors, a Command, and a Worker per key
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const save = Command.make('save', {
|
|
159
|
+
args: Schema.Struct({ id: Schema.String }),
|
|
160
|
+
run: () =>
|
|
161
|
+
Effect.sleep('1 second').pipe(Effect.as({ _tag: 'Saved' as const })),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const List = Actor.make('List', {
|
|
165
|
+
model: Schema.Struct({
|
|
166
|
+
ids: Schema.Array(Schema.String),
|
|
167
|
+
uploading: Schema.Array(Schema.String),
|
|
168
|
+
next: Schema.Number,
|
|
169
|
+
}),
|
|
170
|
+
message: Schema.TaggedUnion({
|
|
171
|
+
Added: {},
|
|
172
|
+
Removed: { id: Schema.String },
|
|
173
|
+
Saved: {},
|
|
174
|
+
Uploaded: { id: Schema.String },
|
|
175
|
+
}),
|
|
176
|
+
}).build({
|
|
177
|
+
init: () => ({ model: { ids: [], uploading: [], next: 0 } }),
|
|
178
|
+
children: { rows: Actor.many(Row) },
|
|
179
|
+
invoke: ({ model }) => ({
|
|
180
|
+
rows: model.ids.map((id) => ({
|
|
181
|
+
key: id,
|
|
182
|
+
input: { text: `#${id}` },
|
|
183
|
+
props: { uploading: model.uploading.includes(id) },
|
|
184
|
+
})),
|
|
185
|
+
}),
|
|
186
|
+
on: {
|
|
187
|
+
Added: (_, { model }) => {
|
|
188
|
+
const id = `${model.next}`;
|
|
189
|
+
return {
|
|
190
|
+
model: {
|
|
191
|
+
ids: [...model.ids, id],
|
|
192
|
+
uploading: [...model.uploading, id],
|
|
193
|
+
next: model.next + 1,
|
|
194
|
+
},
|
|
195
|
+
command: save({ id }),
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
Removed: ({ id }, { model }) => ({
|
|
199
|
+
model: {
|
|
200
|
+
...model,
|
|
201
|
+
ids: model.ids.filter((kept) => kept !== id),
|
|
202
|
+
uploading: model.uploading.filter((kept) => kept !== id),
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
Saved: () => ({}),
|
|
206
|
+
Uploaded: ({ id }, { model }) => ({
|
|
207
|
+
model: { ...model, uploading: model.uploading.filter((k) => k !== id) },
|
|
208
|
+
}),
|
|
209
|
+
},
|
|
210
|
+
workers: {
|
|
211
|
+
upload: {
|
|
212
|
+
each: ({ model }) => model.uploading,
|
|
213
|
+
run: (self, id) =>
|
|
214
|
+
Effect.sleep('3 seconds').pipe(
|
|
215
|
+
Effect.andThen(self.send({ _tag: 'Uploaded', id })),
|
|
216
|
+
),
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
- `invoke` sits beside the `children` it feeds, and says which keys exist, the Input each new Child starts from, and the Props each is handed. After every Update, a new key Invokes a Row, a key gone stops it, and a key kept keeps its Instance. From then on the Row owns its data.
|
|
223
|
+
- Props are what the parent says now, worked out again after each of its Updates: the Row declares them with `props` beside `input`, and its Update, its Workers' Conditions and its View read them. It never writes them, and a change runs none of its rules. Props equal to the last ones keep the Instance; new ones are not in the History, since Replay works them out again from the parent.
|
|
224
|
+
- Instance IDs are deterministic: `List/rows[0]#1` is the first `rows` keyed `0`. A key Invoked again gets `#2`, so a Message meant for the old Instance is dropped.
|
|
225
|
+
- A Command is an errand, made once with `Command.make`: a name, the Schema of its args and the Effect it runs from them, ending with at most one Message. The Update asks for it with its args, `save({ id })`, which is plain data a test can compare; the args are checked when it runs. Nothing stops it but its Instance stopping, and there is no cancel. An Update may return a list of Commands; they run concurrently.
|
|
226
|
+
- A Worker runs for exactly as long as its Condition holds, read from the Model, State and Props whenever they change: where it is written, at the top or in a State, and within that always, `while` a test is true, or `each` key a function names, one Worker per key. Removing an id from `uploading` interrupts that upload and runs its finalizers; a finished upload Sends `Uploaded`, whose Update takes its key away. Stopping work is changing the Model.
|
|
227
|
+
- The Worker does the work and the Model keeps the story: every step the user could notice is a Message. A Worker must be able to start from the Model alone, because a resume or a start from an earlier History entry starts every Worker whose condition holds. A Worker that waits until a Time in the Model sleeps for `that - now`, so it waits only what is left. A finalizer never Sends.
|
|
228
|
+
|
|
229
|
+
### Drawing it with React
|
|
230
|
+
|
|
231
|
+
```tsx
|
|
232
|
+
export const AuthView = View.make(Auth, ({ _tag, state, children }) => {
|
|
233
|
+
switch (_tag) {
|
|
234
|
+
case 'Checking':
|
|
235
|
+
return <Spinner />;
|
|
236
|
+
case 'Anonymous':
|
|
237
|
+
return <LoginView instance={children.login} />;
|
|
238
|
+
case 'Authenticated':
|
|
239
|
+
return (
|
|
240
|
+
<>
|
|
241
|
+
<p>Signed in as {state.user}</p>
|
|
242
|
+
<TodosView instance={children.todos} />
|
|
243
|
+
</>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const RowView = View.make(Row, ({ model, props, root }) => (
|
|
249
|
+
<li {...root} data-uploading={props.uploading}>
|
|
250
|
+
{model.text}
|
|
251
|
+
</li>
|
|
252
|
+
));
|
|
253
|
+
const MotionRow = motion.create(RowView);
|
|
254
|
+
|
|
255
|
+
const DemoApp = toReact(Auth, AuthView, ServerLive);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
- One function draws every State. It gets `_tag`, `model`, `state`, `props`, `children` (Handles; arrays for keyed Children, each with its `key`), `send` and `root`; a `switch` on `_tag` narrows `state` and `children`. The DOM stays across a Transition. It never sees Capabilities, so it draws a Replay exactly like the live app.
|
|
259
|
+
- A Child is placed with `<RowView instance={handle} />`. A parent View hands it only where to draw; data from the parent is the Row's Props, which the parent Actor hands it, so its Update and Workers see the same.
|
|
260
|
+
- Spreading `root` on the View's own element hands it the parent's `ref` and `style`, so `motion.create(RowView)` animates it, with no wrapper element, and it stays memoized. A View whose Instance has stopped keeps drawing its last data, so `AnimatePresence` can play its exit.
|
|
261
|
+
- A View re-renders only when its own Instance's Model, State, Props or set of Children changes, or its `ref` or `style` does. Many Messages handled in a row make one render.
|
|
262
|
+
- `DemoApp.useRuntime()` returns `{ history, branch, head, running, shown, show, start, stop, clear, workers, failure }` from anywhere on the page. Each `branch` entry is only its Envelope: `destination`, `message`, `at`, and `source`, the Instance that Sent it and what in it did; `history.outcome(id)` works out whether it was handled, ignored or dropped, and `history.snapshotAt(id)` takes the app apart there. `show(id)` draws the app right after that entry, on any Branch, while the live app runs on; `stop()` closes every Worker, Command and Capability; `start(id)` moves the Head to that entry and goes live there, growing a new Branch, and `start()` carries on from the Head, at its Time. `workers(id)` says what an Instance's Workers are doing live: running, finished, or failed. A start, stop or clear that fails, like the Runtime's first start or `start(id)` with no such entry, is logged with `console.error` and kept as `failure`, an Effect `Cause`, until one succeeds.
|
|
263
|
+
- Work that must survive a stop belongs in a Worker: Commands running at a stop are lost. The outside world is not rewound.
|
|
264
|
+
- What changes over time arrives as Messages: a Worker Sends a `Tick` or its progress, and the View draws the Model. motion can smooth what it draws from one Message to the next.
|
|
265
|
+
- `toReact` does not compile until the Layer covers every Capability the tree still needs.
|
|
266
|
+
|
|
267
|
+
### Drawing it before it runs
|
|
268
|
+
|
|
269
|
+
```tsx
|
|
270
|
+
const DemoApp = toReact(Auth, AuthView, ServerLive, { saved });
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
- The app is drawn before any Runtime exists: with no History, right after init; with a saved one, at its Head, replayed through Suspense. A server or a static build gets the same HTML the client hydrates, and `useRuntime` reads the saved History from the start, so a Timeline is drawn there too. Until the Runtime starts, a Send is dropped.
|
|
274
|
+
- `saved` is what `history.save()` gives: its entries and its Head, each Message, and each Instance of a Restore's Snapshot, written through its Actor's Schemas and read back through them on load. So the encoded side of every Schema must be JSON-safe, and then it is plain JSON: a `Date` as `Schema.DateFromString`, an `Option` as `Schema.OptionFromNullOr`. A saved History never changes, and one whose Head is not one of its entries fails to load. The Runtime carries on live at the Head, with Time going on from the Head's. `History.restored(Auth, snapshot, at).save()` makes the lightest one, a History of one Restore right after init, for an app that saves only its Snapshot; init stays the first Step. Where the History is kept is the app's business.
|
|
275
|
+
- When the last mount closes, the History is left under the app's name, the root Actor's by default, and the next mount of an app with that name carries on from it: the same component mounted again, or a new one after a hot reload, whose code replays the old Messages. A Message the new code handles differently warns and goes on. Give `name` when two apps share a root Actor and must not share a History; two apps of one name mounted at once is a console error.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Actor as ActorOf, AnyActor, Behavior, KidsByState, Many, OpenOfBehavior, Requires, Single, Tagged } from './types.ts';
|
|
2
|
+
import { Schema } from 'effect';
|
|
3
|
+
/** An Actor's Schemas, waiting for the behavior that runs it. */
|
|
4
|
+
export interface Defined<Name extends string, Input, Props, Model, State extends Tagged, Msg extends Tagged, R extends Requires> {
|
|
5
|
+
readonly build: <const B extends Behavior<Input, Props, Model, State, Msg, R, B>>(behavior: B) => ActorOf<Name, Input, Props, Model, State, Msg, KidsByState<State, B>, OpenOfBehavior<R, B>>;
|
|
6
|
+
}
|
|
7
|
+
/** A Schema Kabuki can read and write without Capabilities and at once. */
|
|
8
|
+
type Codec<T> = Schema.Codec<T, unknown, never, never>;
|
|
9
|
+
/**
|
|
10
|
+
* What `make` takes: every type the behavior is checked against, as codecs,
|
|
11
|
+
* and the Capabilities the Actor's own code may use. Every value is checked
|
|
12
|
+
* against them too, as it is made: each Input and Props, each Model and
|
|
13
|
+
* State from init or an Update, and each Message as it is Sent. A saved
|
|
14
|
+
* History holds the encoded side of the Props, Model, State and Message
|
|
15
|
+
* Schemas, so that side must be JSON-safe.
|
|
16
|
+
*/
|
|
17
|
+
export interface Shape<Input, Props, Model, State, Msg, R extends Requires> {
|
|
18
|
+
/** The Capabilities its own Workers, Commands and Provides use. Nothing else is in reach. */
|
|
19
|
+
readonly requires?: R;
|
|
20
|
+
/** What a parent hands this Actor when it Invokes it, which init starts from. */
|
|
21
|
+
readonly input?: Codec<Input>;
|
|
22
|
+
/** What a parent hands this Actor for as long as it lives, worked out again after each of the parent's Updates. */
|
|
23
|
+
readonly props?: Codec<Props>;
|
|
24
|
+
readonly model?: Codec<Model>;
|
|
25
|
+
readonly message?: Codec<Msg>;
|
|
26
|
+
readonly state?: Codec<State>;
|
|
27
|
+
}
|
|
28
|
+
declare function make<const Name extends string, State extends Tagged = Single, Model = {}, Msg extends Tagged = never, const R extends Requires = readonly [], Input = void, Props = void>(name: Name, shape: Shape<Input, Props, Model, State, Msg, R>): Defined<Name, Input, Props, Model, State, Msg, R>;
|
|
29
|
+
/** A keyed Child slot: the parent Invokes one Instance of `actor` per key. */
|
|
30
|
+
declare const many: <A extends AnyActor>(actor: A) => Many<A>;
|
|
31
|
+
/**
|
|
32
|
+
* An Actor, in two steps. `make` takes its Schemas and Requires, which fixes
|
|
33
|
+
* every type. `build` takes how it behaves, at the top for as long as the
|
|
34
|
+
* Instance lives and in one block per State for as long as that State lasts:
|
|
35
|
+
* its Provides, Children, Invocation, Updates under `on`, and Workers.
|
|
36
|
+
*/
|
|
37
|
+
export declare const Actor: {
|
|
38
|
+
make: typeof make;
|
|
39
|
+
many: typeof many;
|
|
40
|
+
};
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=actor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actor.d.ts","sourceRoot":"","sources":["../../../src/core/actor/actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,KAAK,IAAI,OAAO,EAChB,QAAQ,EACR,QAAQ,EAKR,WAAW,EACX,IAAI,EACJ,cAAc,EACd,QAAQ,EACR,MAAM,EAGN,MAAM,EAEP,MAAM,YAAY,CAAC;AACpB,OAAO,EAAU,MAAM,EAAa,MAAM,QAAQ,CAAC;AAEnD,iEAAiE;AACjE,MAAM,WAAW,OAAO,CACtB,IAAI,SAAS,MAAM,EACnB,KAAK,EACL,KAAK,EACL,KAAK,EACL,KAAK,SAAS,MAAM,EACpB,GAAG,SAAS,MAAM,EAClB,CAAC,SAAS,QAAQ;IAElB,QAAQ,CAAC,KAAK,EAAE,CACd,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,EAE/D,QAAQ,EAAE,CAAC,KACR,OAAO,CACV,IAAI,EACJ,KAAK,EACL,KAAK,EACL,KAAK,EACL,KAAK,EACL,GAAG,EACH,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,EACrB,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CACrB,CAAC;CACH;AAED,2EAA2E;AAC3E,KAAK,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,QAAQ;IACxE,6FAA6F;IAC7F,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IACtB,iFAAiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9B,mHAAmH;IACnH,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;CAC/B;AAED,iBAAS,IAAI,CACX,KAAK,CAAC,IAAI,SAAS,MAAM,EACzB,KAAK,SAAS,MAAM,GAAG,MAAM,EAC7B,KAAK,GAAG,EAAE,EACV,GAAG,SAAS,MAAM,GAAG,KAAK,EAC1B,KAAK,CAAC,CAAC,SAAS,QAAQ,GAAG,SAAS,EAAE,EACtC,KAAK,GAAG,IAAI,EACZ,KAAK,GAAG,IAAI,EAEZ,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAC/C,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;AAmNrD,8EAA8E;AAC9E,QAAA,MAAM,IAAI,GAAI,CAAC,SAAS,QAAQ,SAAS,CAAC,KAAG,IAAI,CAAC,CAAC,CAGjD,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,KAAK;;;CAAiB,CAAC"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Effect, Schema, SchemaAST } from 'effect';
|
|
2
|
+
function make(name, shape) {
|
|
3
|
+
return {
|
|
4
|
+
build: (behavior) => ({
|
|
5
|
+
name,
|
|
6
|
+
definition: definitionOf(name, shape, behavior),
|
|
7
|
+
}),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** The plain definition the engine works from, out of the Schemas and the levels. */
|
|
11
|
+
const definitionOf = (name, shape, behavior) => {
|
|
12
|
+
const blocks = behavior.state ?? {};
|
|
13
|
+
const states = statesOf(shape.state, blocks);
|
|
14
|
+
const top = slots(behavior.children);
|
|
15
|
+
const lasting = Object.keys(top);
|
|
16
|
+
const children = {};
|
|
17
|
+
const invoke = {};
|
|
18
|
+
const update = {};
|
|
19
|
+
const stateProvides = {};
|
|
20
|
+
const workers = {};
|
|
21
|
+
const addWorkers = (from, state) => {
|
|
22
|
+
for (const [worker, written] of Object.entries(from.workers ?? {})) {
|
|
23
|
+
if (workers[worker])
|
|
24
|
+
throw new Error(`[@kestack/kabuki] ${name}: Worker "${worker}" is written twice`);
|
|
25
|
+
workers[worker] = workerOf(written, state);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
if (behavior.on)
|
|
29
|
+
update['*'] = behavior.on;
|
|
30
|
+
addWorkers(behavior, undefined);
|
|
31
|
+
for (const tag of states) {
|
|
32
|
+
const block = blocks[tag] ?? {};
|
|
33
|
+
const own = slots(block.children);
|
|
34
|
+
for (const slot of Object.keys(own))
|
|
35
|
+
if (slot in top)
|
|
36
|
+
throw new Error(`[@kestack/kabuki] ${name}: Child "${slot}" is written at the top and in ${tag}`);
|
|
37
|
+
children[tag] = { ...top, ...own };
|
|
38
|
+
invoke[tag] = merged(behavior.invoke, block.invoke);
|
|
39
|
+
if (block.on)
|
|
40
|
+
update[tag] = block.on;
|
|
41
|
+
stateProvides[tag] = block.provides;
|
|
42
|
+
addWorkers(block, tag);
|
|
43
|
+
}
|
|
44
|
+
// TypeScript cannot see this: a rule that destructures its Message hides
|
|
45
|
+
// its whole `on` from inference. So it is checked once here, and warned.
|
|
46
|
+
for (const tag of tagsOf(shape.message))
|
|
47
|
+
if (!Object.values(update).some((rules) => tag in rules))
|
|
48
|
+
Effect.runSync(Effect.logWarning(`[@kestack/kabuki] ${name}: Message "${tag}" has no rule at the top or in any State, so it is always ignored`));
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
states,
|
|
52
|
+
requires: shape.requires ?? [],
|
|
53
|
+
check: {
|
|
54
|
+
input: checkerOf(name)(shape.input),
|
|
55
|
+
props: checkerOf(name)(shape.props),
|
|
56
|
+
model: checkerOf(name)(shape.model),
|
|
57
|
+
state: checkerOf(name)(shape.state),
|
|
58
|
+
message: checkerOf(name)(shape.message),
|
|
59
|
+
},
|
|
60
|
+
encoding: {
|
|
61
|
+
props: encodingOf(shape.props),
|
|
62
|
+
model: encodingOf(shape.model),
|
|
63
|
+
state: encodingOf(shape.state),
|
|
64
|
+
message: encodingOf(shape.message),
|
|
65
|
+
},
|
|
66
|
+
init: behavior.init ?? (() => ({})),
|
|
67
|
+
update,
|
|
68
|
+
workers,
|
|
69
|
+
provides: behavior.provides,
|
|
70
|
+
stateProvides,
|
|
71
|
+
children,
|
|
72
|
+
lasting,
|
|
73
|
+
invoke,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
const slots = (children) => Object.fromEntries(Object.entries(children ?? {}).map(([slot, child]) => [
|
|
77
|
+
slot,
|
|
78
|
+
isMany(child)
|
|
79
|
+
? { definition: child.actor.definition, many: true }
|
|
80
|
+
: { definition: child.definition, many: false },
|
|
81
|
+
]));
|
|
82
|
+
/** The top-level Invocation and a State's own, as one. */
|
|
83
|
+
const merged = (top, own) => !top ? own : !own ? top : (data) => ({ ...top(data), ...own(data) });
|
|
84
|
+
/**
|
|
85
|
+
* The State tags: a tagged union's cases, else every State a block is
|
|
86
|
+
* written for. An Actor without States has the one, `Single`.
|
|
87
|
+
*/
|
|
88
|
+
const statesOf = (schema, blocks) => {
|
|
89
|
+
if (!schema)
|
|
90
|
+
return ['Single'];
|
|
91
|
+
const cases = schema.cases;
|
|
92
|
+
return Object.keys(cases ?? blocks);
|
|
93
|
+
};
|
|
94
|
+
/** Throws, naming the Actor and what the value is, unless `schema` describes it. */
|
|
95
|
+
const checkerOf = (name) => (schema) => {
|
|
96
|
+
if (!schema)
|
|
97
|
+
return () => { };
|
|
98
|
+
const decode = Schema.decodeUnknownSync(Schema.toType(schema));
|
|
99
|
+
return (value, what) => {
|
|
100
|
+
try {
|
|
101
|
+
decode(value);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw new TypeError(`[@kestack/kabuki] ${name}: ${what} does not match its Schema\n${error.message}`);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
const AS_IS = { encode: (value) => value, decode: (value) => value };
|
|
109
|
+
/** A value to its Schema's encoded side and back, for a saved History. */
|
|
110
|
+
const encodingOf = (schema) => schema
|
|
111
|
+
? {
|
|
112
|
+
encode: Schema.encodeUnknownSync(schema),
|
|
113
|
+
decode: Schema.decodeUnknownSync(schema),
|
|
114
|
+
}
|
|
115
|
+
: AS_IS;
|
|
116
|
+
/** The tags a Message Schema names: one per tagged struct in its union. */
|
|
117
|
+
const tagsOf = (schema) => {
|
|
118
|
+
if (!schema)
|
|
119
|
+
return [];
|
|
120
|
+
const { ast } = schema;
|
|
121
|
+
return (SchemaAST.isUnion(ast) ? ast.types : [ast]).flatMap((member) => SchemaAST.isObjects(member)
|
|
122
|
+
? member.propertySignatures.flatMap(({ name, type }) => name === '_tag' && SchemaAST.isLiteral(type)
|
|
123
|
+
? [String(type.literal)]
|
|
124
|
+
: [])
|
|
125
|
+
: []);
|
|
126
|
+
};
|
|
127
|
+
const ONE = [''];
|
|
128
|
+
const NONE = [];
|
|
129
|
+
/**
|
|
130
|
+
* A Worker's Condition as the keys it names: one, `''`, for every kind but
|
|
131
|
+
* `each`. Written in a State's block, it names none outside that State.
|
|
132
|
+
*/
|
|
133
|
+
const workerOf = (worker, state) => {
|
|
134
|
+
const { run } = worker;
|
|
135
|
+
const own = 'each' in worker
|
|
136
|
+
? worker.each
|
|
137
|
+
: 'while' in worker
|
|
138
|
+
? (data) => (worker.while(data) ? ONE : NONE)
|
|
139
|
+
: () => ONE;
|
|
140
|
+
const kind = 'each' in worker
|
|
141
|
+
? 'each'
|
|
142
|
+
: 'while' in worker
|
|
143
|
+
? 'while'
|
|
144
|
+
: state === undefined
|
|
145
|
+
? 'instance'
|
|
146
|
+
: 'state';
|
|
147
|
+
if (state === undefined)
|
|
148
|
+
return { kind, keys: own, run };
|
|
149
|
+
return {
|
|
150
|
+
kind,
|
|
151
|
+
state,
|
|
152
|
+
keys: (data) => (data.state._tag === state ? own(data) : NONE),
|
|
153
|
+
run,
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
const isMany = (slot) => '_tag' in slot && slot._tag === 'Many';
|
|
157
|
+
/** A keyed Child slot: the parent Invokes one Instance of `actor` per key. */
|
|
158
|
+
const many = (actor) => ({
|
|
159
|
+
_tag: 'Many',
|
|
160
|
+
actor,
|
|
161
|
+
});
|
|
162
|
+
/**
|
|
163
|
+
* An Actor, in two steps. `make` takes its Schemas and Requires, which fixes
|
|
164
|
+
* every type. `build` takes how it behaves, at the top for as long as the
|
|
165
|
+
* Instance lives and in one block per State for as long as that State lasts:
|
|
166
|
+
* its Provides, Children, Invocation, Updates under `on`, and Workers.
|
|
167
|
+
*/
|
|
168
|
+
export const Actor = { make, many };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
|
+
import type { Command as Made } from './types.ts';
|
|
3
|
+
/** A Schema Kabuki can read without Capabilities and at once. */
|
|
4
|
+
type Codec<T> = Schema.Codec<T, unknown, never, never>;
|
|
5
|
+
/**
|
|
6
|
+
* An errand an Update asks for. `make` takes its name, the Schema of its args
|
|
7
|
+
* and the Effect it runs from them, and gives what an Update calls with the
|
|
8
|
+
* args: `{ command: Search({ query }) }`.
|
|
9
|
+
*/
|
|
10
|
+
export declare const Command: {
|
|
11
|
+
make: {
|
|
12
|
+
<Args, Out, Ids = never>(name: string, options: {
|
|
13
|
+
readonly args: Codec<Args>;
|
|
14
|
+
readonly run: (args: Args) => Effect.Effect<Out, never, Ids>;
|
|
15
|
+
}): (args: Args) => Made<Out, Ids>;
|
|
16
|
+
<Out, Ids = never>(name: string, options: {
|
|
17
|
+
readonly args?: never;
|
|
18
|
+
readonly run: () => Effect.Effect<Out, never, Ids>;
|
|
19
|
+
}): () => Made<Out, Ids>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export type Command<Out, Ids = never> = Made<Out, Ids>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../src/core/actor/command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,KAAK,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,YAAY,CAAC;AAclD,iEAAiE;AACjE,KAAK,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;AAiDvD;;;;GAIG;AACH,eAAO,MAAM,OAAO;;SApCjB,IAAI,EAAE,GAAG,EAAE,GAAG,gBACP,MAAM,WACH;YACP,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YAC3B,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;SAC9D,GACA,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC;SAChC,GAAG,EAAE,GAAG,gBACD,MAAM,WACH;YACP,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC;YACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;SACpD,GACA,MAAM,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC;;CAuBM,CAAC;AAChC,MAAM,MAAM,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
|
+
import { RUN } from './types.js';
|
|
3
|
+
/** Throws, naming the Command, unless `schema` describes its args. */
|
|
4
|
+
const checkerOf = (name, schema) => {
|
|
5
|
+
if (!schema)
|
|
6
|
+
return () => { };
|
|
7
|
+
const decode = Schema.decodeUnknownSync(Schema.toType(schema));
|
|
8
|
+
return (args) => {
|
|
9
|
+
try {
|
|
10
|
+
decode(args);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw new TypeError(`[@kestack/kabuki] Command ${name}: its args do not match their Schema\n${error.message}`);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
const make = (name, options) => {
|
|
18
|
+
const check = checkerOf(name, options.args);
|
|
19
|
+
return (args) => Object.defineProperty({ name, args }, RUN, {
|
|
20
|
+
value: Effect.suspend(() => {
|
|
21
|
+
check(args);
|
|
22
|
+
return options.run(args);
|
|
23
|
+
}),
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* An errand an Update asks for. `make` takes its name, the Schema of its args
|
|
28
|
+
* and the Effect it runs from them, and gives what an Update calls with the
|
|
29
|
+
* args: `{ command: Search({ query }) }`.
|
|
30
|
+
*/
|
|
31
|
+
export const Command = { make };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { Actor } from './actor.ts';
|
|
2
|
+
export { Command } from './command.ts';
|
|
3
|
+
export { RUN } from './types.ts';
|
|
4
|
+
export type { Defined } from './actor.ts';
|
|
5
|
+
export type { ActorOf, AnyActor, Data, ActorDefinition, Encoding, Many, Only, Self, SlotDefinition, Tagged, Types, Worker, WorkerDefinition, Workers, } from './types.ts';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/actor/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACjC,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,YAAY,EACV,OAAO,EACP,QAAQ,EACR,IAAI,EACJ,eAAe,EACf,QAAQ,EACR,IAAI,EACJ,IAAI,EACJ,IAAI,EACJ,cAAc,EACd,MAAM,EACN,KAAK,EACL,MAAM,EACN,gBAAgB,EAChB,OAAO,GACR,MAAM,YAAY,CAAC"}
|