@gjermundgaraba/effect-actions 0.19.0 → 0.21.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.
Files changed (326) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -48
  3. package/dist/Action.mjs +88 -44
  4. package/dist/ActionCli.d.mts +70 -35
  5. package/dist/ActionCli.mjs +53 -34
  6. package/dist/ActionHttp.d.mts +70 -38
  7. package/dist/ActionHttp.mjs +76 -25
  8. package/dist/ActionMcp.d.mts +56 -24
  9. package/dist/ActionMcp.mjs +104 -61
  10. package/dist/ActionRpc.d.mts +222 -57
  11. package/dist/ActionRpc.mjs +301 -49
  12. package/dist/ActionToolkit.d.mts +22 -7
  13. package/dist/ActionToolkit.mjs +37 -14
  14. package/dist/Authentication.d.mts +42 -12
  15. package/dist/Authentication.mjs +38 -32
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.d.mts +1 -3
  19. package/dist/Testing.mjs +2 -3
  20. package/dist/Webhook.d.mts +146 -0
  21. package/dist/Webhook.mjs +72 -0
  22. package/dist/authentication/provider.d.mts +35 -12
  23. package/dist/authentication/provider.mjs +2 -3
  24. package/dist/authentication/refusal.mjs +3 -4
  25. package/dist/cli/commands.d.mts +9 -7
  26. package/dist/cli/commands.mjs +85 -35
  27. package/dist/contract/ast.mjs +5 -3
  28. package/dist/contract/authorizer.d.mts +8 -0
  29. package/dist/contract/authorizer.mjs +20 -0
  30. package/dist/contract/call.d.mts +9 -8
  31. package/dist/contract/call.mjs +49 -8
  32. package/dist/contract/callback.d.mts +7 -0
  33. package/dist/contract/callback.mjs +27 -0
  34. package/dist/contract/caller.d.mts +12 -3
  35. package/dist/contract/caller.mjs +23 -5
  36. package/dist/contract/canonical.mjs +10 -0
  37. package/dist/contract/codec.d.mts +9 -0
  38. package/dist/contract/codec.mjs +9 -0
  39. package/dist/contract/codes.mjs +81 -0
  40. package/dist/contract/declared.d.mts +0 -2
  41. package/dist/contract/declared.mjs +26 -11
  42. package/dist/contract/digest.mjs +6 -0
  43. package/dist/contract/errors.d.mts +9 -5
  44. package/dist/contract/errors.mjs +2 -3
  45. package/dist/contract/feed.d.mts +127 -0
  46. package/dist/contract/feed.mjs +249 -0
  47. package/dist/contract/hints.mjs +1 -2
  48. package/dist/contract/hub.d.mts +94 -0
  49. package/dist/contract/hub.mjs +133 -0
  50. package/dist/contract/idempotency.d.mts +47 -0
  51. package/dist/contract/idempotency.mjs +99 -0
  52. package/dist/contract/implementation.d.mts +73 -51
  53. package/dist/contract/implementation.mjs +94 -60
  54. package/dist/contract/input-check.d.mts +15 -0
  55. package/dist/contract/input-check.mjs +16 -0
  56. package/dist/contract/lease.mjs +28 -0
  57. package/dist/contract/listed.d.mts +4 -0
  58. package/dist/contract/listed.mjs +22 -0
  59. package/dist/contract/log.d.mts +41 -0
  60. package/dist/contract/log.mjs +144 -0
  61. package/dist/contract/media.d.mts +1 -3
  62. package/dist/contract/media.mjs +1 -2
  63. package/dist/contract/names.mjs +15 -6
  64. package/dist/contract/occurrence.d.mts +29 -0
  65. package/dist/contract/occurrence.mjs +45 -0
  66. package/dist/contract/pages.d.mts +13 -0
  67. package/dist/contract/pages.mjs +39 -0
  68. package/dist/contract/progress.d.mts +26 -0
  69. package/dist/contract/progress.mjs +72 -0
  70. package/dist/contract/push.d.mts +20 -0
  71. package/dist/contract/push.mjs +96 -0
  72. package/dist/contract/receiving.d.mts +29 -0
  73. package/dist/contract/receiving.mjs +42 -0
  74. package/dist/contract/recent.mjs +12 -0
  75. package/dist/contract/reported.mjs +16 -0
  76. package/dist/contract/serving.mjs +63 -0
  77. package/dist/contract/status.mjs +15 -7
  78. package/dist/contract/stream.d.mts +34 -0
  79. package/dist/contract/stream.mjs +31 -0
  80. package/dist/contract/subscribe.d.mts +45 -0
  81. package/dist/contract/subscribe.mjs +50 -0
  82. package/dist/contract/transient.mjs +28 -0
  83. package/dist/contract/watch.d.mts +18 -0
  84. package/dist/contract/watch.mjs +105 -0
  85. package/dist/contract/webhooks.d.mts +42 -0
  86. package/dist/contract/webhooks.mjs +53 -0
  87. package/dist/http/client.d.mts +37 -14
  88. package/dist/http/client.mjs +52 -14
  89. package/dist/http/events.d.mts +56 -0
  90. package/dist/http/events.mjs +102 -0
  91. package/dist/http/placed.d.mts +36 -0
  92. package/dist/http/placed.mjs +71 -0
  93. package/dist/http/schema-errors.mjs +2 -3
  94. package/dist/http/streamed.d.mts +21 -0
  95. package/dist/http/streamed.mjs +94 -0
  96. package/dist/http/watch.mjs +50 -0
  97. package/dist/mcp/bridge.mjs +283 -0
  98. package/dist/mcp/client.d.mts +30 -99
  99. package/dist/mcp/client.mjs +60 -134
  100. package/dist/mcp/events.mjs +142 -0
  101. package/dist/mcp/progress.mjs +34 -0
  102. package/dist/mcp/protocol.mjs +40 -15
  103. package/dist/mcp/reply.d.mts +79 -0
  104. package/dist/mcp/reply.mjs +99 -0
  105. package/dist/mcp/request.d.mts +40 -0
  106. package/dist/mcp/request.mjs +51 -0
  107. package/dist/mcp/watch.mjs +115 -0
  108. package/dist/rpc/watch.mjs +39 -0
  109. package/dist/stdio/console.mjs +14 -7
  110. package/dist/testing/memory.d.mts +1 -3
  111. package/dist/testing/memory.mjs +4 -5
  112. package/dist/webhook/address.mjs +47 -0
  113. package/dist/webhook/attempt.mjs +65 -0
  114. package/dist/webhook/receiver.mjs +67 -0
  115. package/dist/webhook/signing.mjs +28 -0
  116. package/dist/webhook/stored.mjs +57 -0
  117. package/dist/webhook/subscriptions.mjs +342 -0
  118. package/docs/Action-failures.md +2 -2
  119. package/docs/Action-streams.md +115 -0
  120. package/docs/Action.md +58 -124
  121. package/docs/ActionCli-failures.md +1 -2
  122. package/docs/ActionCli.md +83 -88
  123. package/docs/ActionHttp-failures.md +3 -3
  124. package/docs/ActionHttp.md +90 -70
  125. package/docs/ActionMcp-failures.md +2 -1
  126. package/docs/ActionMcp.md +120 -92
  127. package/docs/ActionRpc-events.md +126 -0
  128. package/docs/ActionRpc-failures.md +1 -2
  129. package/docs/ActionRpc.md +108 -79
  130. package/docs/ActionToolkit-failures.md +1 -2
  131. package/docs/ActionToolkit.md +33 -90
  132. package/docs/Authentication-advanced.md +57 -61
  133. package/docs/Authentication-failures.md +5 -4
  134. package/docs/Authentication.md +23 -58
  135. package/docs/Event-failures.md +15 -0
  136. package/docs/Event.md +281 -0
  137. package/docs/README.md +51 -82
  138. package/docs/Testing-failures.md +1 -5
  139. package/docs/Testing.md +126 -86
  140. package/docs/Webhook-failures.md +8 -0
  141. package/docs/Webhook.md +319 -0
  142. package/docs/setup-failures.md +2 -3
  143. package/docs/setup.md +32 -53
  144. package/examples/README.md +12 -37
  145. package/examples/assistant/README.md +21 -26
  146. package/examples/assistant/src/agent.ts +66 -24
  147. package/examples/assistant/src/cli.ts +7 -4
  148. package/examples/assistant/src/commands.ts +9 -4
  149. package/examples/assistant/src/identity/Identity.ts +5 -4
  150. package/examples/assistant/src/identity/authorize.ts +13 -5
  151. package/examples/assistant/src/identity/handlers.ts +3 -2
  152. package/examples/assistant/src/identity/permissions.ts +9 -3
  153. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  154. package/examples/assistant/src/scripted.ts +7 -4
  155. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  156. package/examples/assistant/src/tasks/handlers.ts +9 -5
  157. package/examples/assistant/src/tasks/store.ts +38 -11
  158. package/examples/assistant/src/toolkit.ts +11 -7
  159. package/examples/assistant/test/commands.test.ts +53 -27
  160. package/examples/assistant/test/identity/identity.test.ts +27 -11
  161. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  162. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  163. package/examples/assistant/test/toolkit.test.ts +104 -75
  164. package/examples/build-events/README.md +83 -0
  165. package/examples/build-events/src/Api.ts +27 -0
  166. package/examples/build-events/src/Rpc.ts +26 -0
  167. package/examples/build-events/src/builds/Builds.ts +108 -0
  168. package/examples/build-events/src/builds/events.ts +54 -0
  169. package/examples/build-events/src/builds/handlers.ts +58 -0
  170. package/examples/build-events/src/builds/log.ts +30 -0
  171. package/examples/build-events/src/builds/status.ts +28 -0
  172. package/examples/build-events/src/builds/store.ts +130 -0
  173. package/examples/build-events/src/cli.ts +18 -0
  174. package/examples/build-events/src/commands.ts +32 -0
  175. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  176. package/examples/build-events/src/deploys/approvers.ts +55 -0
  177. package/examples/build-events/src/deploys/events.ts +32 -0
  178. package/examples/build-events/src/deploys/handlers.ts +38 -0
  179. package/examples/build-events/src/identity/Identity.ts +29 -0
  180. package/examples/build-events/src/identity/members.ts +7 -0
  181. package/examples/build-events/src/identity/verifier.ts +22 -0
  182. package/examples/build-events/src/notifier/Notices.ts +26 -0
  183. package/examples/build-events/src/notifier/routes.ts +71 -0
  184. package/examples/build-events/src/notifier.ts +48 -0
  185. package/examples/build-events/src/routes.ts +71 -0
  186. package/examples/build-events/src/server.ts +62 -0
  187. package/examples/build-events/test/builds/events.test.ts +68 -0
  188. package/examples/build-events/test/builds/log.test.ts +71 -0
  189. package/examples/build-events/test/builds/progress.test.ts +57 -0
  190. package/examples/build-events/test/builds/status.test.ts +42 -0
  191. package/examples/build-events/test/commands.test.ts +103 -0
  192. package/examples/build-events/test/deploys/events.test.ts +52 -0
  193. package/examples/build-events/test/http.test.ts +95 -0
  194. package/examples/build-events/test/notifier.test.ts +80 -0
  195. package/examples/build-events/test/routes.test.ts +96 -0
  196. package/examples/build-events/test/rpc.test.ts +84 -0
  197. package/examples/build-events/test/support/as.ts +8 -0
  198. package/examples/build-events/test/support/memory.ts +43 -0
  199. package/examples/build-events/test/webhooks.test.ts +143 -0
  200. package/examples/kitchen-sink/README.md +178 -166
  201. package/examples/kitchen-sink/src/Actions.ts +11 -3
  202. package/examples/kitchen-sink/src/Api.ts +12 -6
  203. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  204. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  205. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  206. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  207. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  208. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  209. package/examples/kitchen-sink/src/agent.ts +14 -6
  210. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  211. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  212. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  213. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  214. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  215. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  216. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  217. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  218. package/examples/kitchen-sink/src/browser.ts +10 -4
  219. package/examples/kitchen-sink/src/cli.ts +28 -12
  220. package/examples/kitchen-sink/src/config.ts +1 -1
  221. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  222. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  223. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  224. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  225. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  226. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  227. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  228. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  229. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  230. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  231. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  232. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  233. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  234. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  235. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  236. package/examples/kitchen-sink/src/memory.ts +11 -2
  237. package/examples/kitchen-sink/src/ops.ts +10 -5
  238. package/examples/kitchen-sink/src/request-policy.ts +25 -9
  239. package/examples/kitchen-sink/src/routes.ts +67 -27
  240. package/examples/kitchen-sink/src/sdk.ts +22 -12
  241. package/examples/kitchen-sink/src/seed.ts +15 -9
  242. package/examples/kitchen-sink/src/server.ts +24 -10
  243. package/examples/kitchen-sink/src/stdio.ts +9 -3
  244. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  245. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  246. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  247. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  248. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  249. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  250. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  251. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  252. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  253. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  254. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  255. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  256. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  257. package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
  258. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  259. package/examples/kitchen-sink/test/support/as.ts +9 -0
  260. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  261. package/examples/kitchen-sink/test/support/services.ts +17 -0
  262. package/examples/live-rpc/README.md +33 -35
  263. package/examples/live-rpc/src/Rpc.ts +9 -5
  264. package/examples/live-rpc/src/browser.ts +39 -11
  265. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  266. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  267. package/examples/live-rpc/src/chat/events.ts +74 -0
  268. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  269. package/examples/live-rpc/src/chat/room.ts +9 -5
  270. package/examples/live-rpc/src/chat/store.ts +51 -14
  271. package/examples/live-rpc/src/client.ts +22 -5
  272. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  273. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  274. package/examples/live-rpc/src/page.ts +58 -12
  275. package/examples/live-rpc/src/routes.ts +14 -10
  276. package/examples/live-rpc/src/server.ts +15 -5
  277. package/examples/live-rpc/test/browser.test.ts +127 -50
  278. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  279. package/examples/live-rpc/test/routes.test.ts +130 -46
  280. package/examples/live-rpc/test/server.test.ts +94 -33
  281. package/examples/mcp-tools/README.md +24 -38
  282. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  283. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  284. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  285. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  286. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  287. package/examples/mcp-tools/src/routes.ts +8 -6
  288. package/examples/mcp-tools/src/server.ts +12 -5
  289. package/examples/mcp-tools/src/stdio.ts +3 -2
  290. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  291. package/examples/mcp-tools/test/routes.test.ts +140 -81
  292. package/examples/notes-service/README.md +30 -45
  293. package/examples/notes-service/src/Api.ts +1 -0
  294. package/examples/notes-service/src/browser.ts +15 -14
  295. package/examples/notes-service/src/cli.ts +8 -3
  296. package/examples/notes-service/src/commands.ts +46 -20
  297. package/examples/notes-service/src/identity/Identity.ts +2 -2
  298. package/examples/notes-service/src/identity/authorize.ts +6 -2
  299. package/examples/notes-service/src/identity/handlers.ts +3 -2
  300. package/examples/notes-service/src/identity/users.ts +2 -1
  301. package/examples/notes-service/src/identity/verifier.ts +12 -6
  302. package/examples/notes-service/src/notes/Notes.ts +46 -22
  303. package/examples/notes-service/src/notes/handlers.ts +18 -9
  304. package/examples/notes-service/src/notes/store.ts +70 -43
  305. package/examples/notes-service/src/routes.ts +7 -5
  306. package/examples/notes-service/src/server.ts +12 -5
  307. package/examples/notes-service/src/status/Status.ts +1 -1
  308. package/examples/notes-service/src/status/handlers.ts +5 -3
  309. package/examples/notes-service/src/stdio.ts +7 -3
  310. package/examples/notes-service/src/worker.ts +6 -3
  311. package/examples/notes-service/test/commands.test.ts +164 -91
  312. package/examples/notes-service/test/notes/as.ts +3 -1
  313. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  314. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  315. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  316. package/examples/notes-service/test/routes.test.ts +106 -71
  317. package/examples/notes-service/test/worker.test.ts +21 -13
  318. package/examples/quickstart/README.md +2 -6
  319. package/examples/quickstart/src/Api.ts +1 -0
  320. package/examples/quickstart/src/client.ts +6 -3
  321. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  322. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  323. package/examples/quickstart/src/routes.ts +3 -2
  324. package/examples/quickstart/src/server.ts +6 -2
  325. package/examples/quickstart/test/routes.test.ts +3 -2
  326. package/package.json +19 -17
package/docs/Action.md CHANGED
@@ -1,24 +1,16 @@
1
1
  # Action
2
2
 
3
- `Action.make` defines a contract: what an action takes and answers, whether it changes anything,
4
- and who may call it. `Action.implement` binds contracts to handlers, behind an authorizer. Every
5
- surface takes the implementation it returns, or a list of them, and `Action.client` calls one in
6
- process.
3
+ `Action.make` defines a contract: what an action takes and answers, whether it changes anything, and who may call it. `Action.implement` binds contracts to handlers, behind an authorizer. Every surface takes the implementation it returns, or a list of them, and `Action.client` calls one in process.
7
4
 
8
- A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search
9
- [Action-failures.md](Action-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`:
10
- each declaration's JSDoc carries its rules and gotchas, and this page only what an example
11
- cannot show.
5
+ A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [Action-failures.md](Action-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show. An action reporting progress or answering a stream is on [Action-streams.md](Action-streams.md); what happens apart from a call, which a subscriber watches, is an event type, on [Event.md](Event.md).
12
6
 
13
7
  ## Define a contract
14
8
 
15
- One `Action.make` per action, in the domain's contract file. Export the domain's actions as one
16
- `as const` list ([notes-service/src/notes/Notes.ts](../examples/notes-service/src/notes/Notes.ts)):
17
- `implement` and every surface take lists.
9
+ One `Action.make` per action, in the domain's contract file. Export the domain's actions as one `as const` list ([notes-service/src/notes/Notes.ts](../examples/notes-service/src/notes/Notes.ts)): `implement` and every surface take lists.
18
10
 
19
11
  ```ts example=quickstart/src/greeting/Greeting.ts
20
- import { Schema } from "effect";
21
12
  import * as Action from "@gjermundgaraba/effect-actions/Action";
13
+ import { Schema } from "effect";
22
14
 
23
15
  // A contract: what the action takes and answers, whether it changes anything, and who may
24
16
  // call it. It imports no server code, so any client can import it, a browser page included.
@@ -31,48 +23,26 @@ export const Greet = Action.make("greet", {
31
23
  });
32
24
  ```
33
25
 
34
- [notes-service/src/notes/Notes.ts](../examples/notes-service/src/notes/Notes.ts) adds declared
35
- errors with their statuses, described fields and the list;
36
- [kitchen-sink/src/issues/Issues.ts](../examples/kitchen-sink/src/issues/Issues.ts) uses every
37
- option. The rules of each option, `input`'s optional fields and `error`'s tags included, are on
38
- `Action.Options`; where an action must be listed to be served, and matching by object identity,
39
- are on `Action.make`. The list each surface takes lives in one file beside the contracts
40
- ([kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts)).
26
+ [notes-service/src/notes/Notes.ts](../examples/notes-service/src/notes/Notes.ts) adds declared errors with their statuses, described fields and the list; [kitchen-sink/src/issues/Issues.ts](../examples/kitchen-sink/src/issues/Issues.ts) uses every option. The rules of each option, `input`'s optional fields and `error`'s tags included, are on `Action.Options`; where an action must be listed to be served, and matching by object identity, are on `Action.make`. The list each surface takes lives in one file beside the contracts ([kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts)).
41
27
 
42
- - **List an action as a tool only where a model may hold what it takes and returns.** A tool
43
- call passes both through the model's context and records its arguments on the current span,
44
- a `Schema.Redacted` value included.
28
+ - **List an action as a tool only where a model may hold what it takes and returns.** A tool call passes both through the model's context, and `ActionToolkit`, through Effect's toolkit, records its arguments on the current span, a `Schema.Redacted` value included.
45
29
 
46
30
  ## Say who may call an action
47
31
 
48
- `caller: Action.Anyone` makes a public action; an identity service, `caller: CurrentUser`, a
49
- protected one, whose identity a remote surface gets by authenticating the request
50
- ([Authentication.md](Authentication.md#declare-how-callers-sign-in)) and a local surface, which
51
- has no one to authenticate, from the host ([ActionToolkit.md](ActionToolkit.md#name-the-caller),
52
- [ActionCli.md](ActionCli.md#name-the-caller), [ActionMcp.md](ActionMcp.md#serve-tools-over-stdio)).
32
+ `caller: Action.Anyone` makes a public action; an identity service, `caller: CurrentUser`, a protected one, whose identity a remote surface gets by authenticating the request ([Authentication.md](Authentication.md#declare-how-callers-sign-in)) and a local surface, which has no one to authenticate, from the host ([ActionToolkit.md](ActionToolkit.md#name-the-caller), [ActionCli.md](ActionCli.md#name-the-caller), [ActionMcp.md](ActionMcp.md#serve-tools-over-stdio)).
53
33
 
54
- `CurrentUser` is declared in
55
- [notes-service/src/identity/Identity.ts](../examples/notes-service/src/identity/Identity.ts), and
56
- a public action in [notes-service/src/status/Status.ts](../examples/notes-service/src/status/Status.ts).
57
- The rules of an identity, never provided at startup, are on `Action.Options.caller`; a test
58
- sends each caller's credential instead
59
- ([Testing.md](Testing.md#test-as-a-signed-in-caller)).
34
+ `CurrentUser` is declared in [notes-service/src/identity/Identity.ts](../examples/notes-service/src/identity/Identity.ts), and a public action in [notes-service/src/status/Status.ts](../examples/notes-service/src/status/Status.ts). The rules of an identity, never provided at startup, are on `Action.Options.caller`; a test sends each caller's credential instead ([Testing.md](Testing.md#test-as-a-signed-in-caller)).
60
35
 
61
- - **One protected contract serves callers with a token over HTTP and a local host without
62
- one**, never a public copy of it: the host provides the caller around `runStdio`
63
- ([notes-service/src/stdio.ts](../examples/notes-service/src/stdio.ts)); a second audience is
64
- `Operator` in [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts).
36
+ - **One protected contract serves callers with a token over HTTP and a local host without one**, never a public copy of it: the host provides the caller around `runStdio` ([notes-service/src/stdio.ts](../examples/notes-service/src/stdio.ts)); a second audience is `Operator` in [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts).
65
37
 
66
38
  ## Implement actions
67
39
 
68
- `Action.implement` binds a handler to one action, or a record of handlers keyed by name to a
69
- list. Either may instead be a builder: an Effect returning the handler or the record, whose
70
- services are startup services. What a handler yields per call, the caller first, is a
71
- request-time requirement.
40
+ `Action.implement` binds a handler to one action, or a record of handlers keyed by name to a list. Either may instead be a builder: an Effect returning the handler or the record, whose services are startup services. What a handler yields per call, the caller first, is a request-time requirement.
72
41
 
73
42
  ```ts example=notes-service/src/notes/handlers.ts
74
- import { Effect } from "effect";
75
43
  import * as Action from "@gjermundgaraba/effect-actions/Action";
44
+ import { Effect } from "effect";
45
+
76
46
  import { authorize } from "../identity/authorize.js";
77
47
  import { CurrentUser } from "../identity/Identity.js";
78
48
  import { Notes } from "./Notes.js";
@@ -95,47 +65,44 @@ export const notes = Action.implement(
95
65
 
96
66
  return { notes: owned.map(({ id, title }) => ({ id, title })) };
97
67
  }),
98
- getNote: ({ id }) => Effect.flatMap(CurrentUser, (user) => store.get(user.id, id)),
68
+ getNote: ({ id }) =>
69
+ Effect.flatMap(CurrentUser, (user) => store.get(user.id, id)),
99
70
  createNote: ({ requestId, title, body }) =>
100
- Effect.flatMap(CurrentUser, (user) => store.create(user.id, title, body, requestId)),
71
+ Effect.flatMap(CurrentUser, (user) =>
72
+ store.create(user.id, title, body, requestId)
73
+ ),
101
74
  updateNote: ({ id, body, revision }) =>
102
- Effect.flatMap(CurrentUser, (user) => store.update(user.id, id, body, revision)),
103
- deleteNote: ({ id }) => Effect.flatMap(CurrentUser, (user) => store.remove(user.id, id)),
104
- importNotes: ({ notes }) =>
75
+ Effect.flatMap(CurrentUser, (user) =>
76
+ store.update(user.id, id, body, revision)
77
+ ),
78
+ deleteNote: ({ id }) =>
79
+ Effect.flatMap(CurrentUser, (user) => store.remove(user.id, id)),
80
+ importNotes: ({ notes: imported }) =>
105
81
  Effect.map(
106
- Effect.flatMap(CurrentUser, (user) => store.createAll(user.id, notes)),
107
- (created) => ({ notes: created }),
82
+ Effect.flatMap(CurrentUser, (user) =>
83
+ store.createAll(user.id, imported)
84
+ ),
85
+ (created) => ({ notes: created })
108
86
  ),
109
87
  };
110
88
  }),
111
- { authorize },
89
+ { authorize }
112
90
  );
113
91
  ```
114
92
 
115
- A plain handler, no builder, is
116
- [quickstart/src/greeting/handlers.ts](../examples/quickstart/src/greeting/handlers.ts). The
117
- builder's rules, once per layer graph, a scope per call, passing the builder to `implement`
118
- itself, calling through `Action.client` and never the function you passed, are on
119
- `Action.implement`.
93
+ A plain handler, no builder, is [quickstart/src/greeting/handlers.ts](../examples/quickstart/src/greeting/handlers.ts). The builder's rules, once per layer graph, a scope per call, passing the builder to `implement` itself, calling through `Action.client` and never the function you passed, are on `Action.implement`.
120
94
 
121
- - **One implementation may hold public and protected actions**, and actions only some
122
- surfaces serve: group by domain.
123
- - **Leave a handler's `Effect.fn` unnamed** unless you need handler-only timing. The span named
124
- after the action covers authorization, the handler and call cleanup, carrying `action.name`
125
- and `action.read_only` on it and its logs. Credential verification and input decoding run
126
- before it, so a request they reject has no action span; response encoding runs after it, so
127
- it can fail after a successful one.
95
+ - **One implementation may hold public and protected actions**, and actions only some surfaces serve: group by domain.
96
+ - **Leave a handler's `Effect.fn` unnamed** unless you need handler-only timing. The span named after the action covers authorization, the handler and call cleanup, carrying `action.name` and `action.read_only` on it and its logs. Credential verification and input decoding run before it, so a request they reject has no action span; response encoding runs after it, so it can fail after a successful one.
128
97
 
129
98
  ## Authorize calls
130
99
 
131
- An implementation holding a protected action states `{ authorize }`: a function of the called
132
- action that succeeds, or fails with a refusal; no surface has a policy of its own. An
133
- implementation of public actions alone takes none
134
- ([notes-service/src/status/handlers.ts](../examples/notes-service/src/status/handlers.ts)).
100
+ An implementation holding a protected action states `{ authorize }`: a function of the called action that succeeds, or fails with a refusal; no surface has a policy of its own. An implementation of public actions alone takes none ([notes-service/src/status/handlers.ts](../examples/notes-service/src/status/handlers.ts)).
135
101
 
136
102
  ```ts example=notes-service/src/identity/authorize.ts
137
- import { Effect } from "effect";
138
103
  import * as Action from "@gjermundgaraba/effect-actions/Action";
104
+ import { Effect } from "effect";
105
+
139
106
  import { CurrentUser, scopes } from "./Identity.js";
140
107
 
141
108
  // The one rule, read from each contract: a read needs the read scope, anything else the write
@@ -147,73 +114,40 @@ export const authorize = Effect.fn("authorize")(function* (action: Action.Any) {
147
114
  const user = yield* CurrentUser;
148
115
 
149
116
  if (!user.scopes.includes(scope)) {
150
- return yield* new Action.Forbidden({ message: `Requires ${scope}.`, scopes: [scope] });
117
+ return yield* new Action.Forbidden({
118
+ message: `Requires ${scope}.`,
119
+ scopes: [scope],
120
+ });
151
121
  }
152
122
  });
153
123
  ```
154
124
 
155
- Actions under another rule go in another `Action.implement` with its own `authorize`, or
156
- `Action.allowAll`, not a branch on `action.name`. What a `Forbidden` with and without `scopes`
157
- answers on each surface is on `Forbidden`.
158
- Reading a store once, by an `authorize` that is an Effect returning the function, is
159
- [assistant/src/identity/authorize.ts](../examples/assistant/src/identity/authorize.ts); that
160
- rule, and that a limit is the handler's, are on `Action.implement`.
161
-
162
- - **A caller the authorizer would refuse can still receive an `InvalidInput` describing the
163
- input's schema**, since input is decoded before the authorizer runs. To refuse or limit before
164
- decoding, use the layer's `middleware`
165
- ([ActionHttp.md](ActionHttp.md#split-a-binding-across-layers-and-add-middleware),
166
- [ActionRpc.md](ActionRpc.md#serve-the-rpcs)).
167
- - **A limiter is a service the builder yields, apart from the store**
168
- ([kitchen-sink/src/issues/write-limit.ts](../examples/kitchen-sink/src/issues/write-limit.ts),
169
- [kitchen-sink/src/memory.ts](../examples/kitchen-sink/src/memory.ts)): state kept in a builder
170
- lasts only within one layer graph.
171
- - **A check on one record belongs in the handler's data access**
172
- ([notes-service/src/notes/store.ts](../examples/notes-service/src/notes/store.ts)). So does a
173
- rule across callers, such as uniqueness.
125
+ Actions under another rule go in another `Action.implement` with its own `authorize`, or `Action.allowAll`, not a branch on `action.name`. What a `Forbidden` with and without `scopes` answers on each surface is on `Forbidden`. Reading a store once, by an `authorize` that is an Effect returning the function, is [assistant/src/identity/authorize.ts](../examples/assistant/src/identity/authorize.ts); that rule, and that a limit is the handler's, are on `Action.implement`.
126
+
127
+ - **A caller the authorizer would refuse can still receive an `InvalidInput` describing the input's schema**, since input is decoded before the authorizer runs. To refuse or limit before decoding, use the layer's `middleware` ([ActionHttp.md](ActionHttp.md#split-a-binding-across-layers-and-add-middleware), [ActionRpc.md](ActionRpc.md#serve-the-rpcs)).
128
+ - **A limiter is a service the builder yields, apart from the store** ([kitchen-sink/src/issues/write-limit.ts](../examples/kitchen-sink/src/issues/write-limit.ts), [kitchen-sink/src/memory.ts](../examples/kitchen-sink/src/memory.ts)): state kept in a builder lasts only within one layer graph.
129
+ - **A check on one record belongs in the handler's data access** ([notes-service/src/notes/store.ts](../examples/notes-service/src/notes/store.ts)). So does a rule across callers, such as uniqueness.
174
130
 
175
131
  ## Fail with declared and built-in errors
176
132
 
177
- A handler fails with the errors its action lists in `error`, and with the three built-in
178
- errors, `InvalidInput`, `Unauthenticated` and `Forbidden`. What a refusal answers on each
179
- surface is on `Action.Unauthenticated` and `Action.Forbidden`; what a declared error and bad
180
- input answer, on each surface's `layer`, `layerHttp`, `runStdio`, `make` or `command`; when to
181
- fail with `InvalidInput` yourself, on that class, as
182
- [kitchen-sink/src/attachments/handlers.ts](../examples/kitchen-sink/src/attachments/handlers.ts)
183
- does.
184
-
185
- - **Map a client's `HttpClientError` and `SchemaError` to one declared error a caller can act
186
- on** ([mcp-tools/src/platform/print-farm.ts](../examples/mcp-tools/src/platform/print-farm.ts)).
187
- As defects, a model reads only a generic text.
188
- - **Internal failures are never sent** over HTTP, MCP and RPC: what reaches the log and the
189
- call's `ErrorReporter`s is on `ActionHttp.layer`, `ActionMcp.layerHttp` and `ActionRpc.layer`;
190
- a Toolkit and a CLI make a defect the call's.
133
+ A handler fails with the errors its action lists in `error`, and with the three built-in errors, `InvalidInput`, `Unauthenticated` and `Forbidden`. What a refusal answers on each surface is on `Action.Unauthenticated` and `Action.Forbidden`; what a declared error and bad input answer, on each surface's `layer`, `layerHttp`, `runStdio`, `make` or `command`; when to fail with `InvalidInput` yourself, on that class, as [kitchen-sink/src/attachments/handlers.ts](../examples/kitchen-sink/src/attachments/handlers.ts) does.
134
+
135
+ - **Add the errors a module adds to every action it serves, a proxy's or a store's, to contracts written elsewhere with `Action.withErrors`**, `pipe(action, Action.withErrors([Unavailable]))`, rather than restating each contract. It returns a new contract, which the module then implements and lists; its rules are on `Action.withErrors`.
136
+ - **Map a client's `HttpClientError` and `SchemaError` to one declared error a caller can act on** ([mcp-tools/src/platform/print-farm.ts](../examples/mcp-tools/src/platform/print-farm.ts)). As defects, a model reads only a generic text.
137
+ - **Internal failures are never sent** over HTTP, MCP and RPC: what reaches the log and the call's `ErrorReporter`s is on `ActionHttp.layer`, `ActionMcp.layerHttp` and `ActionRpc.layer`; a Toolkit and a CLI make a defect the call's.
138
+
139
+ ## Answer a retried call once
140
+
141
+ A write a caller may send again, after a dropped connection or a timeout, names a string field of its input as its `idempotencyKey`. The caller makes the key at random for the call and sends it again on each retry: a repeat with the same input is answered as the first call was, its handler run once, on every surface, since the key travels with the input. An implementation of such an action requires `Action.Idempotency` at startup, where the answers are kept: provide `Action.layerIdempotency` around the surfaces, as `Event.layerMemory` is provided; in a Durable Object, the Cloudflare package's `ActionDurableObject.make` provides it. What is kept, whose a key is, and what a repeat with another input answers, are on the option.
191
142
 
192
143
  ## Call actions in process
193
144
 
194
- `Action.client(implementations)` gives the methods `ActionHttp.client` gives, for a job, an
195
- agent or a test, without a transport. It is acquired where builders live:
196
- [kitchen-sink/src/agent.ts](../examples/kitchen-sink/src/agent.ts) in a scoped program,
197
- [notes-service/test/notes/in-process.test.ts](../examples/notes-service/test/notes/in-process.test.ts)
198
- under `it.effect`. The rules are on `Action.client`; testing with it is
199
- [Testing.md](Testing.md#test-an-implementation-in-process).
145
+ `Action.client(implementations)` gives the methods `ActionHttp.client` gives, for a job, an agent or a test, without a transport. It is acquired where builders live: [kitchen-sink/src/agent.ts](../examples/kitchen-sink/src/agent.ts) in a scoped program, [notes-service/test/notes/in-process.test.ts](../examples/notes-service/test/notes/in-process.test.ts) under `it.effect`. The rules are on `Action.client`; testing with it is [Testing.md](Testing.md#test-an-implementation-in-process).
200
146
 
201
147
  ## Share one build across servers
202
148
 
203
- Inside one `HttpRouter.serve` or one `Testing.layer`, a builder runs once, however many surfaces
204
- serve it, a toolkit's `layer` provided to the routes included. Each `HttpRouter.serve` is a layer
205
- graph of its own, the layers Effect builds together, each once, which shares nothing it builds
206
- with another graph: a builder's run, a built authorizer, or an in-memory store of yours.
207
-
208
- - **With one server, put everything the process runs inside the layer it serves**, a job or an
209
- agent beside the routes included; why a job merged beside `HttpRouter.serve` is not enough is
210
- on `Action.layer`.
211
- - **Otherwise, provide `Action.layer(implementations)` and the services they share above the
212
- server and its siblings**: a second server, a program beside it, several `Testing.layer`s.
213
- [kitchen-sink/src/server.ts](../examples/kitchen-sink/src/server.ts) does it for two ports with
214
- the `builders` layer of [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts),
215
- and [kitchen-sink/test/routes.test.ts](../examples/kitchen-sink/test/routes.test.ts) with two
216
- `Testing.layer`s, asserting one run of a builder both ports serve.
217
- - **Inside one layer graph, a builder runs with the services of whichever surface builds it
218
- first**, and services provided around one surface reach the others: provide startup services
219
- once, for every surface. Where two must differ is on `Action.layer`.
149
+ Inside one `HttpRouter.serve` or one `Testing.layer`, a builder runs once, however many surfaces serve it, a toolkit's `layer` provided to the routes included. Each `HttpRouter.serve` is a layer graph of its own, the layers Effect builds together, each once, which shares nothing it builds with another graph: a builder's run, a built authorizer, or an in-memory store of yours.
150
+
151
+ - **With one server, put everything the process runs inside the layer it serves**, a job or an agent beside the routes included; why a job merged beside `HttpRouter.serve` is not enough is on `Action.layer`.
152
+ - **Otherwise, provide `Action.layer(implementations)` and the services they share above the server and its siblings**: a second server, a program beside it, several `Testing.layer`s. [kitchen-sink/src/server.ts](../examples/kitchen-sink/src/server.ts) does it for two ports with the `builders` layer of [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts), and [kitchen-sink/test/routes.test.ts](../examples/kitchen-sink/test/routes.test.ts) with two `Testing.layer`s, asserting one run of a builder both ports serve.
153
+ - **Inside one layer graph, a builder runs with the services of whichever surface builds it first**, and services provided around one surface reach the others: provide startup services once, for every surface. Where two must differ is on `Action.layer`.
@@ -1,7 +1,6 @@
1
1
  # ActionCli failure modes
2
2
 
3
- What you see when something is wrong, and the fix. Search for a fragment of the message.
4
- The tasks are in [ActionCli.md](ActionCli.md).
3
+ What you see when something is wrong, and the fix. Search for a fragment of the message. The tasks are in [ActionCli.md](ActionCli.md).
5
4
 
6
5
  ## Flags and input
7
6
 
package/docs/ActionCli.md CHANGED
@@ -1,27 +1,24 @@
1
1
  # ActionCli
2
2
 
3
- Native Effect CLI commands for actions, with flags derived from each action's input. `make` and
4
- `command` take implementations and run the handler in the command's own process, behind the same
5
- `authorize`: for a local tool, or an operator console that has the services itself. `remote` and
6
- `remoteCommand` take an HTTP binding and call a server, running nothing locally: for users of a
7
- service you already serve. `make` and `remote` derive one command per action; which actions a
8
- surface serves is on `Action.make`.
3
+ Native Effect CLI commands for actions, with flags derived from each action's input. `make` and `command` take implementations and run the handler in the command's own process, behind the same `authorize`: for a local tool, or an operator console that has the services itself. `remote` and `remoteCommand` take an HTTP binding and call a server, running nothing locally: for users of a service you already serve. `make` and `remote` derive one command per action; which actions a surface serves is on `Action.make`.
9
4
 
10
5
  A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [ActionCli-failures.md](ActionCli-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
11
6
 
12
7
  ## Run actions as local commands
13
8
 
14
- Pass implementations to `make`; the host provides the services and the caller on the command,
15
- and runs it with Effect's own `Command.run`:
9
+ Pass implementations to `make`; the host provides the services and the caller on the command, and runs it with Effect's own `Command.run`:
16
10
 
17
11
  ```ts example=assistant/src/cli.ts
18
12
  import { userInfo } from "node:os";
13
+
19
14
  import { NodeRuntime, NodeServices } from "@effect/platform-node";
15
+ import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
20
16
  import { Effect, Layer } from "effect";
21
17
  import { Command } from "effect/cli";
22
- import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
18
+
23
19
  import { commands } from "./commands.js";
24
- import { CurrentCaller, type Operator } from "./identity/Identity.js";
20
+ import { CurrentCaller } from "./identity/Identity.js";
21
+ import type { Operator } from "./identity/Identity.js";
25
22
  import { Permissions } from "./identity/permissions.js";
26
23
  import { tasksFile } from "./platform/tasks-file.js";
27
24
 
@@ -33,7 +30,7 @@ const operator: Operator = { kind: "operator", id: userInfo().username };
33
30
 
34
31
  const assistant = commands.pipe(
35
32
  Command.provide(Layer.mergeAll(tasksFile, Permissions.layerMemory)),
36
- Command.provideSync(CurrentCaller, operator),
33
+ Command.provideSync(CurrentCaller, operator)
37
34
  );
38
35
 
39
36
  // A command's defect, such as a tasks file that does not decode, `runMain` reports on stdout.
@@ -42,81 +39,90 @@ const assistant = commands.pipe(
42
39
  Command.run(assistant, { version: "1.0.0" }).pipe(
43
40
  Effect.provide(NodeServices.layer),
44
41
  ActionCli.logToStderr,
45
- NodeRuntime.runMain,
42
+ NodeRuntime.runMain()
46
43
  );
47
44
  ```
48
45
 
49
- The aggregate is [assistant/src/commands.ts](../examples/assistant/src/commands.ts), and
50
- [kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts) an operator console. For one
51
- action, or a tree of your own under native `Command.withSubcommands`, use
52
- `command(implementations, action, options?)`. Where services go, what each run builds and what a
53
- command prints on which stream are on `ActionCli.command`; what `runMain` reports, a command's
54
- defect included, on `ActionCli.logToStderr`; matching a failure after `Command.run`, on
55
- `ActionCli.UserError`.
46
+ The aggregate is [assistant/src/commands.ts](../examples/assistant/src/commands.ts), and [kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts) an operator console. For one action, or a tree of your own under native `Command.withSubcommands`, use `command(implementations, action, options?)`. Where services go, what each run builds and what a command prints on which stream are on `ActionCli.command`; what `runMain` reports, a command's defect included, on `ActionCli.logToStderr`; matching a failure after `Command.run`, on `ActionCli.UserError`.
56
47
 
57
- - **Test commands without a process** with `Command.runWith`, `TestConsole` and
58
- `Stdio.layerTest`: [assistant/test/commands.test.ts](../examples/assistant/test/commands.test.ts).
48
+ - **Test commands without a process** with `Command.runWith`, `TestConsole` and `Stdio.layerTest`: [assistant/test/commands.test.ts](../examples/assistant/test/commands.test.ts).
59
49
 
60
50
  ## Call a server from commands
61
51
 
62
- Pass the HTTP binding to `remote`. The program imports the binding alone, never a handler. The
63
- connection is the `client` option, an `ActionHttp.client`
64
- ([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)), its rules on `ActionCli.Calling`'s `client`,
65
- shared by `remote` and `remoteCommand`:
66
- [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts) requires a token,
67
- mapping its absence to an error that says what to do, and
68
- [kitchen-sink/src/cli.ts](../examples/kitchen-sink/src/cli.ts) adds a tenant header, reads an
69
- optional token for a public command. The host provides
70
- `HttpClient` (`NodeHttpClient.layerUndici`) in
71
- [notes-service/src/cli.ts](../examples/notes-service/src/cli.ts).
72
-
73
- - **Split one binding into groups of commands**, one `remote` per group with its own `actions`;
74
- what each group's `client` needs is on `ActionCli.Calling`'s `client`.
75
- - **Test a remote command the same way** as a local one, with `Testing.layer(routes)` provided
76
- as its `HttpClient` ([Testing.md](Testing.md#test-routes-in-memory)). Export the command from
77
- a module apart from the entrypoint that runs it
78
- ([notes-service/test/commands.test.ts](../examples/notes-service/test/commands.test.ts)).
52
+ Pass the HTTP binding to `remote`. The program imports the binding alone, never a handler. The connection is the `client` option, an `ActionHttp.client` ([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)), its rules on `ActionCli.Calling`'s `client`, shared by `remote` and `remoteCommand`: [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts) requires a token, mapping its absence to an error that says what to do, and [kitchen-sink/src/cli.ts](../examples/kitchen-sink/src/cli.ts) adds a tenant header, reads an optional token for a public command. The host provides `HttpClient` (`NodeHttpClient.layerUndici`) in [notes-service/src/cli.ts](../examples/notes-service/src/cli.ts).
53
+
54
+ - **Split one binding into groups of commands**, one `remote` per group with its own `actions`; what each group's `client` needs is on `ActionCli.Calling`'s `client`.
55
+ - **Test a remote command the same way** as a local one, with `Testing.layer(routes)` provided as its `HttpClient` ([Testing.md](Testing.md#test-routes-in-memory)). Export the command from a module apart from the entrypoint that runs it ([notes-service/test/commands.test.ts](../examples/notes-service/test/commands.test.ts)).
56
+
57
+ ## Watch events
58
+
59
+ `make` and `remote` add `watch <event>` for each [event type](Event.md) among the implementations or in the binding, or each one `actions` lists where it is given. Experimental, as [events](Event.md) are.
60
+
61
+ ```ts example=build-events/src/commands.ts
62
+ import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
63
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
64
+ import { Config, Effect } from "effect";
65
+ import { HttpClient, HttpClientRequest } from "effect/http";
66
+
67
+ import { Http } from "./Api.js";
68
+
69
+ // The binding's client, connected to where the server is and as which member, both read from
70
+ // config when a command runs.
71
+ const api = Effect.gen(function* () {
72
+ const baseUrl = yield* Config.String("BUILDS_URL").pipe(
73
+ Config.withDefault("http://127.0.0.1:3000")
74
+ );
75
+
76
+ const token = yield* Config.Redacted("BUILDS_TOKEN");
77
+
78
+ return yield* ActionHttp.client(Http, {
79
+ baseUrl,
80
+ transformClient: HttpClient.mapRequest(
81
+ HttpClientRequest.bearerToken(token)
82
+ ),
83
+ });
84
+ });
85
+
86
+ // A command per action, and `watch <event>` per event type: `builds watch build.finished
87
+ // --pipeline api` holds the event's stream open and prints each occurrence as a line of JSON,
88
+ // its `cursor` included, until it is stopped. `--cursor <cursor>` resumes after that line;
89
+ // omitted, the watch starts from now. The lines pipe into `jq` or a script as they come.
90
+ // `builds start-build --pipeline api` prints each step as a line of JSON on stderr as it
91
+ // starts, and the build on stdout, which a pipe reads alone. `builds tail-log --build-id
92
+ // build-1` prints each line of its log as a line of JSON as it arrives.
93
+ export const commands = ActionCli.remote(Http, { name: "builds", client: api });
94
+ ```
95
+
96
+ [build-events/test/commands.test.ts](../examples/build-events/test/commands.test.ts) runs a watch and stops it. What a watch prints and fails with is on `make` and `remote`.
79
97
 
80
98
  ## Shape flags, input and output
81
99
 
82
100
  ### Flags from the input
83
101
 
84
- A struct or class input gets one flag per top-level field, named after the field in kebab case,
85
- with the field's description as its help, annotated before or after its checks. The field's
86
- encoded JSON value decides the kind of flag:
102
+ A struct or class input gets one flag per top-level field, named after the field in kebab case, with the field's description as its help, annotated before or after its checks. The field's encoded JSON value decides the kind of flag:
87
103
 
88
- | Encoded field | Flag, for a field `tags`, `read`, … |
89
- | ---------------------------------------- | --------------------------------------------------------------- |
90
- | string or template literal | `--name <string>` |
91
- | boolean | `--read`, a switch: `--no-read` or `--read=false` sends `false` |
92
- | union of string literals, or string enum | `--kind <choice>`, one of the values |
93
- | array of strings, numbers or choices | `--tags <value>`, repeated: one element per occurrence |
94
- | anything else, numbers included | `--owner <value>`: JSON the field accepts, or else the text |
104
+ | Encoded field | Flag, for a field `tags`, `read`, … |
105
+ | --- | --- |
106
+ | string or template literal | `--name <string>` |
107
+ | boolean | `--read`, a switch: `--no-read` or `--read=false` sends `false` |
108
+ | union of string literals, or string enum | `--kind <choice>`, one of the values |
109
+ | array of strings, numbers or choices | `--tags <value>`, repeated: one element per occurrence |
110
+ | anything else, numbers included | `--owner <value>`: JSON the field accepts, or else the text |
95
111
 
96
112
  - A switch left out is `false` for a required field, and leaves an optional one out.
97
- - `--help` adds, after the field's description, `Repeat for each value.` on a repeated flag,
98
- `--no-read sends false.` on an optional switch, `Left out, it sends false.` on a required
99
- one, and `A JSON array.` or `A JSON object.` on a
100
- value that is one, an array of objects with its fields: `A JSON array of {id, approved?}.`
101
- - A flag takes the encoded value, which the action's schema then decodes: `"21"` for
102
- `Schema.FiniteFromString`.
103
- - A value flag reads its text as JSON when the field accepts that kind of JSON value
104
- (`--count 2`, `--owner '{"id":"x"}'`, `--points '[{"x":1}]'`), and keeps it as text otherwise
105
- (`--limit auto`): quote a string that would read as JSON, `--id '"123"'` for a
106
- `String | Number` field.
107
- - A repeated flag given no occurrence sends `[]` for a required `Schema.Array` field, and leaves
108
- an optional field out, where `--tags '[]'` alone sends an empty list.
109
- - An input that is not a struct of named fields (a union, a record, a scalar) gets one
110
- `--input <value>` flag carrying the whole encoded input. Left off, the input is `{}`.
113
+ - `--help` adds, after the field's description, `Repeat for each value.` on a repeated flag, `--no-read sends false.` on an optional switch, `Left out, it sends false.` on a required one, and `A JSON array.` or `A JSON object.` on a value that is one, an array of objects with its fields: `A JSON array of {id, approved?}.`
114
+ - A flag takes the encoded value, which the action's schema then decodes: `"21"` for `Schema.FiniteFromString`.
115
+ - A value flag reads its text as JSON when the field accepts that kind of JSON value (`--count 2`, `--owner '{"id":"x"}'`, `--points '[{"x":1}]'`), and keeps it as text otherwise (`--limit auto`): quote a string that would read as JSON, `--id '"123"'` for a `String | Number` field.
116
+ - A repeated flag given no occurrence sends `[]` for a required `Schema.Array` field, and leaves an optional field out, where `--tags '[]'` alone sends an empty list.
117
+ - An input that is not a struct of named fields (a union, a record, a scalar) gets one `--input <value>` flag carrying the whole encoded input. Left off, the input is `{}`.
111
118
 
112
119
  ### Arguments, short flags and stdin
113
120
 
114
- `description`, `render`, `positional`, `aliases` and `stdin` shape one command; in an aggregate
115
- they go under `commands`, keyed by the action's name. Each one's rules are on its own
116
- declaration:
121
+ `description`, `render`, `positional`, `aliases` and `stdin` shape one command; in an aggregate they go under `commands`, keyed by the action's name. Each one's rules are on its own declaration:
117
122
 
118
123
  ```ts example=assistant/src/commands.ts
119
124
  import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
125
+
120
126
  import { identity } from "./identity/handlers.js";
121
127
  import { tasks } from "./tasks/handlers.js";
122
128
 
@@ -130,11 +136,15 @@ export const commands = ActionCli.make([tasks, identity], {
130
136
  // per task for a person, and adds `--json` for the JSON a script reads. `description` is the
131
137
  // command's help, saying what a line holds; the action's stays what a model reads.
132
138
  listTasks: {
133
- description: "Print one line per task: [x] when done, then its id, title and assignee.",
139
+ description:
140
+ "Print one line per task: [x] when done, then its id, title and assignee.",
134
141
  aliases: { assignee: "a" },
135
- render: ({ tasks }) =>
136
- tasks
137
- .map((task) => `${task.done ? "[x]" : "[ ]"} ${task.id} ${task.title} (${task.assignee})`)
142
+ render: ({ tasks: listed }) =>
143
+ listed
144
+ .map(
145
+ (task) =>
146
+ `${task.done ? "[x]" : "[ ]"} ${task.id} ${task.title} (${task.assignee})`
147
+ )
138
148
  .join("\n"),
139
149
  },
140
150
  // `assistant complete-task 2`: an argument instead of `--id 2`.
@@ -147,25 +157,10 @@ export const commands = ActionCli.make([tasks, identity], {
147
157
 
148
158
  ### Add a command of your own
149
159
 
150
- Work every surface should offer, a bulk import for one, is an action first: a command of your
151
- own then only reads the file and calls it. For a syntax the options cannot express, such as a
152
- flag named apart from its field, build the command with native `Command.make`, `Flag` and
153
- `Argument`, and pass it in `subcommands`, whose rules are on `ActionCli.Options`; pipe its
154
- Effect through `toUserError` to fail as a derived command does.
155
- In [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts),
156
- `notes import <file>` reads `Argument.FileText` and calls `importNotes`, and
157
- `notes export <file>` takes `Argument.String("file")` and calls two actions.
160
+ Work every surface should offer, a bulk import for one, is an action first: a command of your own then only reads the file and calls it. For a syntax the options cannot express, such as a flag named apart from its field, build the command with native `Command.make`, `Flag` and `Argument`, and pass it in `subcommands`, whose rules are on `ActionCli.Options`; pipe its Effect through `toUserError` to fail as a derived command does. In [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts), `notes import <file>` reads `Argument.FileText` and calls `importNotes`, and `notes export <file>` takes `Argument.String("file")` and calls two actions.
158
161
 
159
162
  ## Name the caller
160
163
 
161
- `Command.provideSync(CurrentCaller, operator)` names the caller on the command in
162
- [assistant/src/cli.ts](../examples/assistant/src/cli.ts), or
163
- `Command.provideEffect(CurrentCaller, load)` reads it when an action runs
164
- ([Action.md](Action.md#say-who-may-call-an-action)).
165
-
166
- - **A trusted operator is a type, not a flag.** Give the identity a variant only a host builds,
167
- kept out of every verifier's return type, and admit it in the shared `authorize`:
168
- [assistant/src/identity/Identity.ts](../examples/assistant/src/identity/Identity.ts) with
169
- [assistant/src/identity/authorize.ts](../examples/assistant/src/identity/authorize.ts), or
170
- [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts) with
171
- [kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts).
164
+ `Command.provideSync(CurrentCaller, operator)` names the caller on the command in [assistant/src/cli.ts](../examples/assistant/src/cli.ts), or `Command.provideEffect(CurrentCaller, load)` reads it when an action runs ([Action.md](Action.md#say-who-may-call-an-action)).
165
+
166
+ - **A trusted operator is a type, not a flag.** Give the identity a variant only a host builds, kept out of every verifier's return type, and admit it in the shared `authorize`: [assistant/src/identity/Identity.ts](../examples/assistant/src/identity/Identity.ts) with [assistant/src/identity/authorize.ts](../examples/assistant/src/identity/authorize.ts), or [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts) with [kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts).
@@ -1,14 +1,14 @@
1
1
  # ActionHttp failure modes
2
2
 
3
- What you see when something is wrong, and the fix. Search for a fragment of the message.
4
- The tasks are in [ActionHttp.md](ActionHttp.md).
3
+ What you see when something is wrong, and the fix. Search for a fragment of the message. The tasks are in [ActionHttp.md](ActionHttp.md).
5
4
 
6
5
  ## Binding and serving
7
6
 
8
7
  - A route returns 404, which a client method reports as an `HttpClientError` with `DecodeError` or `StatusCodeError`: the path lacks the prefix, no `ActionHttp.layer` call serves the action, or the binding does not hold that very action value, as when a test declares the contract again.
9
- - `Method 'POST' already declared for route '<prefix>/<action>'` when the host builds: two `ActionHttp.layer` calls serve one action; give each layer its own `actions`.
8
+ - `Method 'POST' already declared for route '<prefix>/<action>'`, or `'<prefix>/events/<event>'`, when the host builds: two `ActionHttp.layer` calls serve one action or event type; give each layer its own `actions`.
10
9
  - `Duplicate OpenAPI operationId: <name>` from `OpenApi.fromApi`: two bindings combined into one `HttpApi` share an action name; rename one, or document each binding alone.
11
10
  - `Type 'Handlers<…>' is not assignable to type '`Endpoint not handled: ${string}`'` at `HttpApiBuilder.group(binding.api, …)`, or `Service not found: effect-actions/Authentication/Security/…` when the host builds: a binding is served with the native `HttpApiBuilder`; serve it with `ActionHttp.layer`.
11
+ - `event: effect/http-api/stream/failure` ends an SSE stream: Effect's failure event, an event's source refusing the subscriber after the stream started, or an action answering progress or a stream failing after its first event; its `data` is the failure as an encoded `Cause`, which `ActionHttp.client`'s `watch` or method fails with as declared.
12
12
  - `Unsupported content-type: <type>` as a 415 body: Effect refuses a `Content-Type` other than `application/json`, after authentication and before the handler; send `application/json`.
13
13
 
14
14
  ## Types