@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
@@ -1,14 +1,10 @@
1
1
  # notes-service
2
2
 
3
- A small service as most applications of this library are built: one set of contracts, one
4
- implementation, served as a JSON API and as MCP tools, with a CLI and a browser client on the
5
- same binding. Notes belong to the signed-in user; reading needs the `notes:read` scope and
6
- writing `notes:write`.
3
+ A small service as most applications of this library are built: one set of contracts, one implementation, served as a JSON API and as MCP tools, with a CLI and a browser client on the same binding. Notes belong to the signed-in user; reading needs the `notes:read` scope and writing `notes:write`.
7
4
 
8
5
  ## Run it
9
6
 
10
- From the repository root, after `vp install` (from an installed package, see
11
- [Run one](../README.md#run-one)):
7
+ From the repository root, after `vp install` (from an installed package, see [Run one](../README.md#run-one)):
12
8
 
13
9
  ```sh
14
10
  node --import tsx examples/notes-service/src/server.ts # 127.0.0.1:3000
@@ -34,30 +30,19 @@ claude mcp add --transport http notes http://127.0.0.1:3000/mcp --header "Author
34
30
  codex mcp add notes --url http://127.0.0.1:3000/mcp --bearer-token-env-var NOTES_TOKEN
35
31
  ```
36
32
 
37
- `src/stdio.ts` is another way in: a subprocess the host starts, with a store of its own.
38
- `src/worker.ts` serves the same routes as a `fetch` handler, for a serverless host such as
39
- Cloudflare Workers ([ActionMcp.md](../../docs/ActionMcp.md#serve-tools-over-http)).
33
+ `src/stdio.ts` is another way in: a subprocess the host starts, with a store of its own. `src/worker.ts` serves the same routes as a `fetch` handler, for a serverless host such as Cloudflare Workers ([ActionMcp.md](../../docs/ActionMcp.md#serve-tools-over-http)).
40
34
 
41
- The server reads `HOST` (default `127.0.0.1`), `PORT` (default 3000) and `PUBLIC_URL`, the
42
- address it tells OAuth clients it is at; the CLI reads `NOTES_URL` and `NOTES_TOKEN`. Change the
43
- port and set `PORT`, `PUBLIC_URL` and `NOTES_URL` to match. Every command asks for
44
- `NOTES_TOKEN`, the public `get-status` included: without it a command prints one message on
45
- stderr saying to set it, and exits 1. `--help` needs none.
35
+ The server reads `HOST` (default `127.0.0.1`), `PORT` (default 3000) and `PUBLIC_URL`, the address it tells OAuth clients it is at; the CLI reads `NOTES_URL` and `NOTES_TOKEN`. Change the port and set `PORT`, `PUBLIC_URL` and `NOTES_URL` to match. Every command asks for `NOTES_TOKEN`, the public `get-status` included: without it a command prints one message on stderr saying to set it, and exits 1. `--help` needs none.
46
36
 
47
37
  ## Retry a creation
48
38
 
49
- `createNote({ title, body })` creates a new note on every call. To retry safely, add an optional
50
- `requestId`. Generate it once for a new creation, for example with `crypto.randomUUID()`, and
51
- keep it with the pending operation outside any retry loop. After a lost response, send the
52
- same key and content again. For the CLI, pass `--request-id` and keep it when repeating the
53
- command:
39
+ `createNote({ title, body })` creates a new note on every call. To retry safely, add an optional `requestId`. Generate it once for a new creation, for example with `crypto.randomUUID()`, and keep it with the pending operation outside any retry loop. After a lost response, send the same key and content again. For the CLI, pass `--request-id` and keep it when repeating the command:
54
40
 
55
41
  ```sh
56
42
  echo "First draft" | node --import tsx examples/notes-service/src/cli.ts create-note --request-id plan-1 --title Plan
57
43
  ```
58
44
 
59
- What a key promises, per caller, for changed content and after the note changes, is what
60
- [test/notes/idempotency.test.ts](test/notes/idempotency.test.ts) asserts.
45
+ What a key promises, per caller, for changed content and after the note changes, is what [test/notes/idempotency.test.ts](test/notes/idempotency.test.ts) asserts.
61
46
 
62
47
  ## Layout
63
48
 
@@ -93,27 +78,27 @@ test/
93
78
 
94
79
  ## Where to look
95
80
 
96
- | To see | Read |
97
- | ------------------------------------------------------------- | -------------------------------- |
98
- | A contract with declared errors and their statuses | `src/notes/Notes.ts` |
99
- | Input fields described, for `--help` and a tool's schema | `src/notes/Notes.ts` |
100
- | Handlers getting a service once and the caller per call | `src/notes/handlers.ts` |
101
- | A write safe to retry with the same caller and request key | `src/notes/store.ts` |
102
- | A public action beside protected ones | `src/status/` |
103
- | Authorization from `readOnly`, refusing with a scope | `src/identity/authorize.ts` |
104
- | A token verifier and OAuth discovery read from config | `src/identity/verifier.ts` |
105
- | One implementation served over HTTP and MCP | `src/routes.ts` |
106
- | Config, a body limit and the host's own routes | `src/server.ts`, `src/routes.ts` |
107
- | The same actions over stdio, the host naming the caller | `src/stdio.ts` |
108
- | A serverless entrypoint, the MCP endpoint `requestScoped` | `src/worker.ts`, `src/routes.ts` |
109
- | A CLI derived from the binding: an argument, a field on stdin | `src/commands.ts` |
110
- | A command written by hand beside the derived ones | `src/commands.ts` |
111
- | A bulk import as one action, its command reading a file | `src/notes/`, `src/commands.ts` |
112
- | The CLI's connection, read from config when a command runs | `src/commands.ts` |
113
- | A missing token as one message, not a `ConfigError` report | `src/commands.ts` |
114
- | A promise client handling a declared error by its tag | `src/browser.ts` |
115
- | Testing handlers and authorization with no transport | `test/notes/in-process.test.ts` |
116
- | Testing a revision race, two fibers taking turns | `test/notes/notes.test.ts` |
117
- | Testing routes, tools and refusals in memory | `test/routes.test.ts` |
118
- | Testing commands against the routes in memory | `test/commands.test.ts` |
119
- | Testing `--help` and the connection with config as a Layer | `test/commands.test.ts` |
81
+ | To see | Read |
82
+ | --- | --- |
83
+ | A contract with declared errors and their statuses | `src/notes/Notes.ts` |
84
+ | Input fields described, for `--help` and a tool's schema | `src/notes/Notes.ts` |
85
+ | Handlers getting a service once and the caller per call | `src/notes/handlers.ts` |
86
+ | A write safe to retry with the same caller and request key | `src/notes/store.ts` |
87
+ | A public action beside protected ones | `src/status/` |
88
+ | Authorization from `readOnly`, refusing with a scope | `src/identity/authorize.ts` |
89
+ | A token verifier and OAuth discovery read from config | `src/identity/verifier.ts` |
90
+ | One implementation served over HTTP and MCP | `src/routes.ts` |
91
+ | Config, a body limit and the host's own routes | `src/server.ts`, `src/routes.ts` |
92
+ | The same actions over stdio, the host naming the caller | `src/stdio.ts` |
93
+ | A serverless entrypoint, the MCP endpoint `requestScoped` | `src/worker.ts`, `src/routes.ts` |
94
+ | A CLI derived from the binding: an argument, a field on stdin | `src/commands.ts` |
95
+ | A command written by hand beside the derived ones | `src/commands.ts` |
96
+ | A bulk import as one action, its command reading a file | `src/notes/`, `src/commands.ts` |
97
+ | The CLI's connection, read from config when a command runs | `src/commands.ts` |
98
+ | A missing token as one message, not a `ConfigError` report | `src/commands.ts` |
99
+ | A promise client handling a declared error by its tag | `src/browser.ts` |
100
+ | Testing handlers and authorization with no transport | `test/notes/in-process.test.ts` |
101
+ | Testing a revision race, two fibers taking turns | `test/notes/notes.test.ts` |
102
+ | Testing routes, tools and refusals in memory | `test/routes.test.ts` |
103
+ | Testing commands against the routes in memory | `test/commands.test.ts` |
104
+ | Testing `--help` and the connection with config as a Layer | `test/commands.test.ts` |
@@ -1,4 +1,5 @@
1
1
  import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
2
+
2
3
  import { Login, WhoAmI } from "./identity/Identity.js";
3
4
  import { Notes } from "./notes/Notes.js";
4
5
  import { GetStatus } from "./status/Status.js";
@@ -1,6 +1,7 @@
1
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
1
2
  import { Effect } from "effect";
2
3
  import { HttpClient, HttpClientRequest } from "effect/http";
3
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
4
+
4
5
  import { Http } from "./Api.js";
5
6
  import type { Note } from "./notes/Notes.js";
6
7
 
@@ -12,30 +13,30 @@ import type { Note } from "./notes/Notes.js";
12
13
  export const connect = (token: string, baseUrl?: string) => {
13
14
  // Built once, outside any Effect. Every method is typed from its action.
14
15
  const api = ActionHttp.fetchClient(Http, {
15
- transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken(token)),
16
+ transformClient: HttpClient.mapRequest(
17
+ HttpClientRequest.bearerToken(token)
18
+ ),
16
19
  ...(baseUrl === undefined ? {} : { baseUrl }),
17
20
  });
18
21
 
19
22
  // Each method returns a promise. A failure rejects it with the decoded error, so in a `catch`
20
23
  // `error instanceof Action.Unauthenticated` tells an expired sign-in from anything else.
21
24
  return {
22
- notes: () => Effect.runPromise(api.listNotes()),
25
+ notes: async () => await Effect.runPromise(api.listNotes()),
23
26
 
24
27
  // A declared error can also be handled by its tag before the promise: a missing note is
25
28
  // `undefined` here, and every other failure still rejects.
26
- note: (id: string): Promise<Note | undefined> =>
27
- api.getNote({ id }).pipe(
28
- Effect.catchTag("NoteNotFound", () => Effect.succeed(undefined)),
29
- Effect.runPromise,
29
+ note: async (id: string): Promise<Note | undefined> =>
30
+ await api.getNote({ id }).pipe(
31
+ Effect.catchTag("NoteNotFound", () => Effect.undefined),
32
+ Effect.runPromise
30
33
  ),
31
34
 
32
- // A stale revision is refused, never overwritten: the error carries the current one.
33
- save: (note: Note, body: string) =>
34
- api.updateNote({ id: note.id, body, revision: note.revision }).pipe(
35
- Effect.catchTag("NoteChanged", ({ revision }) =>
36
- Effect.fail(new Error(`Someone else saved revision ${revision}. Reload the note.`)),
37
- ),
38
- Effect.runPromise,
35
+ // A stale revision is refused, never overwritten: the error, `NoteChanged`, carries the
36
+ // current one.
37
+ save: async (note: Note, body: string) =>
38
+ await Effect.runPromise(
39
+ api.updateNote({ id: note.id, body, revision: note.revision })
39
40
  ),
40
41
  };
41
42
  };
@@ -1,7 +1,12 @@
1
- import { NodeHttpClient, NodeRuntime, NodeServices } from "@effect/platform-node";
1
+ import {
2
+ NodeHttpClient,
3
+ NodeRuntime,
4
+ NodeServices,
5
+ } from "@effect/platform-node";
6
+ import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
2
7
  import { Effect } from "effect";
3
8
  import { Command } from "effect/cli";
4
- import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
9
+
5
10
  import { commands } from "./commands.js";
6
11
 
7
12
  // Entrypoint: `node --import tsx examples/notes-service/src/cli.ts list-notes`.
@@ -9,5 +14,5 @@ import { commands } from "./commands.js";
9
14
  Command.run(commands, { version: "1.0.0" }).pipe(
10
15
  Effect.provide([NodeHttpClient.layerUndici, NodeServices.layer]),
11
16
  ActionCli.logToStderr,
12
- NodeRuntime.runMain,
17
+ NodeRuntime.runMain()
13
18
  );
@@ -1,13 +1,17 @@
1
+ import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
2
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
1
3
  import { Config, Console, Effect, FileSystem, Schema } from "effect";
2
4
  import { Argument, CliError, Command } from "effect/cli";
3
5
  import { HttpClient, HttpClientRequest } from "effect/http";
4
- import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
5
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
6
+
6
7
  import { Http } from "./Api.js";
7
8
 
8
9
  // What a command fails with when no token is configured: a tagged error of the CLI's own, printed
9
10
  // on stderr as `MissingToken: <message>` under an `ERROR` heading, and exit 1.
10
- class MissingToken extends Schema.TaggedError<MissingToken>()("MissingToken", {
11
+ class MissingToken extends Schema.TaggedError<
12
+ MissingToken,
13
+ { readonly brand: unique symbol }
14
+ >()("MissingToken", {
11
15
  message: Schema.String,
12
16
  }) {}
13
17
 
@@ -15,7 +19,7 @@ class MissingToken extends Schema.TaggedError<MissingToken>()("MissingToken", {
15
19
  // from config. Every command here, the derived ones and your own, calls through this one.
16
20
  const api = Effect.gen(function* () {
17
21
  const baseUrl = yield* Config.String("NOTES_URL").pipe(
18
- Config.withDefault("http://127.0.0.1:3000"),
22
+ Config.withDefault("http://127.0.0.1:3000")
19
23
  );
20
24
 
21
25
  // Required, since every command but `get-status` is refused without one, and mapped so that a
@@ -24,14 +28,17 @@ const api = Effect.gen(function* () {
24
28
  Effect.mapError(
25
29
  () =>
26
30
  new MissingToken({
27
- message: "NOTES_TOKEN is not set. Set it to your token: export NOTES_TOKEN=<your token>",
28
- }),
29
- ),
31
+ message:
32
+ "NOTES_TOKEN is not set. Set it to your token: export NOTES_TOKEN=<your token>",
33
+ })
34
+ )
30
35
  );
31
36
 
32
37
  return yield* ActionHttp.client(Http, {
33
38
  baseUrl,
34
- transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken(token)),
39
+ transformClient: HttpClient.mapRequest(
40
+ HttpClientRequest.bearerToken(token)
41
+ ),
35
42
  });
36
43
  });
37
44
 
@@ -49,17 +56,26 @@ const unescape = (body: string) => body.replaceAll(/^\\(?=\\*# )/gmu, "");
49
56
  // `FileSystem` every Effect CLI command has.
50
57
  const exportNotes = Command.make(
51
58
  "export",
52
- { file: Argument.String("file").pipe(Argument.withDescription("The Markdown file to write.")) },
59
+ {
60
+ file: Argument.String("file").pipe(
61
+ Argument.withDescription("The Markdown file to write.")
62
+ ),
63
+ },
53
64
  ({ file }) =>
54
65
  Effect.gen(function* () {
55
66
  const client = yield* api;
56
67
  const files = yield* FileSystem.FileSystem;
57
68
  const { notes } = yield* client.listNotes();
58
- const whole = yield* Effect.forEach(notes, ({ id }) => client.getNote({ id }));
69
+
70
+ const whole = yield* Effect.forEach(notes, ({ id }) =>
71
+ client.getNote({ id })
72
+ );
59
73
 
60
74
  yield* files.writeFileString(
61
75
  file,
62
- whole.map((note) => `# ${note.title}\n\n${escape(note.body)}`).join("\n\n"),
76
+ whole
77
+ .map((note) => `# ${note.title}\n\n${escape(note.body)}`)
78
+ .join("\n\n")
63
79
  );
64
80
 
65
81
  yield* Console.log(`Wrote ${whole.length} notes to ${file}`);
@@ -67,10 +83,12 @@ const exportNotes = Command.make(
67
83
  // Context of your own for a failure, on stderr before it. The line prints for every
68
84
  // failure of the command, a file that cannot be written included, so it says only what is
69
85
  // true of them all.
70
- Effect.tapError(() => Console.error(`The export to ${file} did not finish.`)),
86
+ Effect.tapError(() =>
87
+ Console.error(`The export to ${file} did not finish.`)
88
+ ),
71
89
  // So the export fails as a derived command does.
72
- ActionCli.toUserError(Http),
73
- ),
90
+ ActionCli.toUserError(Http)
91
+ )
74
92
  ).pipe(Command.withDescription("Write every note to a file, as Markdown."));
75
93
 
76
94
  // The other half of `export`: it reads such a file and creates its notes with one call of the
@@ -84,19 +102,22 @@ const importNotes = Command.make(
84
102
  "import",
85
103
  {
86
104
  text: Argument.FileText("file").pipe(
87
- Argument.withDescription("A Markdown file as `notes export` writes it."),
105
+ Argument.withDescription("A Markdown file as `notes export` writes it.")
88
106
  ),
89
107
  },
90
108
  ({ text }) =>
91
109
  Effect.gen(function* () {
92
110
  const client = yield* api;
93
111
 
94
- if (text === "") return yield* Console.log("Created 0 notes");
112
+ if (text === "") {
113
+ return yield* Console.log("Created 0 notes");
114
+ }
95
115
 
96
116
  if (!text.startsWith("# ")) {
97
117
  return yield* new CliError.UserError({
98
118
  cause: text.split("\n", 1)[0],
99
- userMessage: "The file does not start with a `# ` title, as `notes export` writes one.",
119
+ userMessage:
120
+ "The file does not start with a `# ` title, as `notes export` writes one.",
100
121
  });
101
122
  }
102
123
 
@@ -109,9 +130,11 @@ const importNotes = Command.make(
109
130
  const { notes: created } = yield* client.importNotes({ notes });
110
131
 
111
132
  yield* Console.log(`Created ${created.length} notes`);
112
- }).pipe(ActionCli.toUserError(Http)),
133
+ }).pipe(ActionCli.toUserError(Http))
113
134
  ).pipe(
114
- Command.withDescription("Create the notes of a file `export` wrote, all or none, at most 100."),
135
+ Command.withDescription(
136
+ "Create the notes of a file `export` wrote, all or none, at most 100."
137
+ )
115
138
  );
116
139
 
117
140
  // A command per action of the binding, a flag per input field, each calling the server over
@@ -123,7 +146,10 @@ export const commands = ActionCli.remote(Http, {
123
146
  commands: {
124
147
  // `notes create-note --title Draft < draft.md`: the body is piped in, not a flag.
125
148
  createNote: { stdin: "body" },
126
- getNote: { positional: ["id"], render: (note) => `# ${note.title}\n\n${note.body}` },
149
+ getNote: {
150
+ positional: ["id"],
151
+ render: (note) => `# ${note.title}\n\n${note.body}`,
152
+ },
127
153
  // `notes update-note <id> --revision 1 < draft.md`: an argument, a flag and stdin together.
128
154
  updateNote: { positional: ["id"], stdin: "body" },
129
155
  },
@@ -1,6 +1,6 @@
1
- import { Context, Schema } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
3
2
  import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
3
+ import { Context, Schema } from "effect";
4
4
 
5
5
  export const scopes = { read: "notes:read", write: "notes:write" } as const;
6
6
 
@@ -16,7 +16,7 @@ export interface User {
16
16
  * provides it per request, and a local host, such as the stdio server, provides it itself.
17
17
  */
18
18
  export class CurrentUser extends Context.Service<CurrentUser, User>()(
19
- "notes-service/CurrentUser",
19
+ "notes-service/CurrentUser"
20
20
  ) {}
21
21
 
22
22
  // How a remote caller proves it is CurrentUser: a bearer token, the default scheme. Clients
@@ -1,5 +1,6 @@
1
- import { Effect } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import { Effect } from "effect";
3
+
3
4
  import { CurrentUser, scopes } from "./Identity.js";
4
5
 
5
6
  // The one rule, read from each contract: a read needs the read scope, anything else the write
@@ -11,6 +12,9 @@ export const authorize = Effect.fn("authorize")(function* (action: Action.Any) {
11
12
  const user = yield* CurrentUser;
12
13
 
13
14
  if (!user.scopes.includes(scope)) {
14
- return yield* new Action.Forbidden({ message: `Requires ${scope}.`, scopes: [scope] });
15
+ return yield* new Action.Forbidden({
16
+ message: `Requires ${scope}.`,
17
+ scopes: [scope],
18
+ });
15
19
  }
16
20
  });
@@ -1,10 +1,11 @@
1
- import { Effect } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import { Effect } from "effect";
3
+
3
4
  import { authorize } from "./authorize.js";
4
5
  import { CurrentUser, WhoAmI } from "./Identity.js";
5
6
 
6
7
  export const identity = Action.implement(
7
8
  WhoAmI,
8
9
  () => Effect.map(CurrentUser, ({ id, scopes }) => ({ id, scopes })),
9
- { authorize },
10
+ { authorize }
10
11
  );
@@ -1,4 +1,5 @@
1
- import { scopes, type User } from "./Identity.js";
1
+ import { scopes } from "./Identity.js";
2
+ import type { User } from "./Identity.js";
2
3
 
3
4
  // DEMO ONLY: the people who may sign in, each under a token that is their name. A real service
4
5
  // has no such table: its verifier reads the user from a signed token.
@@ -1,20 +1,24 @@
1
- import { Config, Effect, Redacted } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
3
2
  import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
3
+ import { Config, Effect, Redacted } from "effect";
4
+
4
5
  import { Login, scopes } from "./Identity.js";
5
6
  import { users } from "./users.js";
6
7
 
7
- const isToken = (token: string): token is keyof typeof users => Object.hasOwn(users, token);
8
+ const isToken = (token: string): token is keyof typeof users =>
9
+ Object.hasOwn(users, token);
8
10
 
9
11
  // DEMO ONLY: a token is a user's name. A real verifier checks a signed token with your
10
12
  // authorization server's library, its audience included. An unknown token is the built-in
11
13
  // `Unauthenticated`: a 401 every client decodes.
12
- const verify = (token: Redacted.Redacted<string>) => {
14
+ const verify = (token: Redacted.Redacted) => {
13
15
  const name = Redacted.value(token);
14
16
 
15
17
  return isToken(name)
16
18
  ? Effect.succeed(users[name])
17
- : Effect.fail(new Action.Unauthenticated({ message: "Unknown demo token." }));
19
+ : Effect.fail(
20
+ new Action.Unauthenticated({ message: "Unknown demo token." })
21
+ );
18
22
  };
19
23
 
20
24
  // Server only: `Login`'s provider. As an OAuth protected resource it also publishes discovery,
@@ -22,7 +26,9 @@ const verify = (token: Redacted.Redacted<string>) => {
22
26
  // is the server's public URL, known only at startup, so it is read from config.
23
27
  export const authenticate = Authentication.layer(Login, verify, {
24
28
  protectedResource: Effect.map(
25
- Config.String("PUBLIC_URL").pipe(Config.withDefault("http://localhost:3000")),
29
+ Config.String("PUBLIC_URL").pipe(
30
+ Config.withDefault("http://localhost:3000")
31
+ ),
26
32
  (url) =>
27
33
  ({
28
34
  resource: `${url}/mcp`,
@@ -30,6 +36,6 @@ export const authenticate = Authentication.layer(Login, verify, {
30
36
  authorizationServers: ["https://auth.example.com"],
31
37
  scopesSupported: [scopes.read, scopes.write],
32
38
  scopesRequired: [scopes.read],
33
- }) satisfies Authentication.ProtectedResource,
39
+ }) satisfies Authentication.ProtectedResource
34
40
  ),
35
41
  });
@@ -1,5 +1,6 @@
1
- import { Schema } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import { Schema } from "effect";
3
+
3
4
  import { CurrentUser } from "../identity/Identity.js";
4
5
 
5
6
  export const Note = Schema.Struct({
@@ -13,23 +14,24 @@ export type Note = typeof Note.Type;
13
14
 
14
15
  // A declared error is part of the contract: every client decodes it as this class, and its
15
16
  // status is what HTTP answers with.
16
- export class NoteNotFound extends Schema.TaggedError<NoteNotFound>()(
17
- "NoteNotFound",
18
- { id: Schema.String },
19
- { httpApiStatus: 404 },
20
- ) {}
21
-
22
- export class NoteChanged extends Schema.TaggedError<NoteChanged>()(
17
+ export class NoteNotFound extends Schema.TaggedError<
18
+ NoteNotFound,
19
+ { readonly brand: unique symbol }
20
+ >()("NoteNotFound", { id: Schema.String }, { httpApiStatus: 404 }) {}
21
+
22
+ export class NoteChanged extends Schema.TaggedError<
23
+ NoteChanged,
24
+ { readonly brand: unique symbol }
25
+ >()(
23
26
  "NoteChanged",
24
27
  { id: Schema.String, revision: Schema.Finite },
25
- { httpApiStatus: 409 },
28
+ { httpApiStatus: 409 }
26
29
  ) {}
27
30
 
28
- export class RequestConflict extends Schema.TaggedError<RequestConflict>()(
29
- "RequestConflict",
30
- { requestId: Schema.String },
31
- { httpApiStatus: 409 },
32
- ) {}
31
+ export class RequestConflict extends Schema.TaggedError<
32
+ RequestConflict,
33
+ { readonly brand: unique symbol }
34
+ >()("RequestConflict", { requestId: Schema.String }, { httpApiStatus: 409 }) {}
33
35
 
34
36
  // A description on an input field is that field's help on every surface: beside its flag or
35
37
  // argument in a command's `--help`, and in the input schema a model reads from the MCP tool
@@ -42,7 +44,9 @@ const Id = Schema.String.annotate({
42
44
  // generic "Expected a value with a length of at least 1". A model and a CLI user read it too.
43
45
  const Title = Schema.String.check(
44
46
  Schema.isMinLength(1, { message: "A note needs a title." }),
45
- Schema.isPattern(/^[^\r\n\u2028\u2029]*$/u, { message: "A title is one line." }),
47
+ Schema.isPattern(/^[^\r\n\u2028\u2029]*$/u, {
48
+ message: "A title is one line.",
49
+ })
46
50
  ).annotate({
47
51
  description: "A short name for the note: one line, at least one character.",
48
52
  });
@@ -53,7 +57,11 @@ const Content = Schema.Struct({ title: Title, body: Body });
53
57
 
54
58
  export const ListNotes = Action.make("listNotes", {
55
59
  description: "List your notes, without their bodies.",
56
- success: { notes: Schema.Array(Schema.Struct({ id: Schema.String, title: Schema.String })) },
60
+ success: {
61
+ notes: Schema.Array(
62
+ Schema.Struct({ id: Schema.String, title: Schema.String })
63
+ ),
64
+ },
57
65
  readOnly: true,
58
66
  caller: CurrentUser,
59
67
  });
@@ -90,9 +98,12 @@ export const UpdateNote = Action.make("updateNote", {
90
98
  "Replace a note's body. Pass the revision you read: the update is refused if the note changed since.",
91
99
  input: {
92
100
  id: Id,
93
- body: Body.annotate({ description: "The text that replaces the note's body." }),
101
+ body: Body.annotate({
102
+ description: "The text that replaces the note's body.",
103
+ }),
94
104
  revision: Schema.Finite.annotate({
95
- description: "The revision you read: 1 for a new note, one more after each update.",
105
+ description:
106
+ "The revision you read: 1 for a new note, one more after each update.",
96
107
  }),
97
108
  },
98
109
  success: Note,
@@ -120,10 +131,16 @@ export const ImportNotes = Action.make("importNotes", {
120
131
  input: {
121
132
  notes: Schema.Array(Content)
122
133
  .check(
123
- Schema.isMinLength(1, { message: "An import holds at least one note." }),
124
- Schema.isMaxLength(100, { message: "An import holds at most 100 notes." }),
134
+ Schema.isMinLength(1, {
135
+ message: "An import holds at least one note.",
136
+ }),
137
+ Schema.isMaxLength(100, {
138
+ message: "An import holds at most 100 notes.",
139
+ })
125
140
  )
126
- .annotate({ description: "The notes to create, in order: from 1 to 100." }),
141
+ .annotate({
142
+ description: "The notes to create, in order: from 1 to 100.",
143
+ }),
127
144
  },
128
145
  success: { notes: Schema.Array(Note) },
129
146
  readOnly: false,
@@ -131,4 +148,11 @@ export const ImportNotes = Action.make("importNotes", {
131
148
  mcp: { destructiveHint: false },
132
149
  });
133
150
 
134
- export const Notes = [ListNotes, GetNote, CreateNote, UpdateNote, DeleteNote, ImportNotes] as const;
151
+ export const Notes = [
152
+ ListNotes,
153
+ GetNote,
154
+ CreateNote,
155
+ UpdateNote,
156
+ DeleteNote,
157
+ ImportNotes,
158
+ ] as const;
@@ -1,5 +1,6 @@
1
- import { Effect } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import { Effect } from "effect";
3
+
3
4
  import { authorize } from "../identity/authorize.js";
4
5
  import { CurrentUser } from "../identity/Identity.js";
5
6
  import { Notes } from "./Notes.js";
@@ -22,18 +23,26 @@ export const notes = Action.implement(
22
23
 
23
24
  return { notes: owned.map(({ id, title }) => ({ id, title })) };
24
25
  }),
25
- getNote: ({ id }) => Effect.flatMap(CurrentUser, (user) => store.get(user.id, id)),
26
+ getNote: ({ id }) =>
27
+ Effect.flatMap(CurrentUser, (user) => store.get(user.id, id)),
26
28
  createNote: ({ requestId, title, body }) =>
27
- Effect.flatMap(CurrentUser, (user) => store.create(user.id, title, body, requestId)),
29
+ Effect.flatMap(CurrentUser, (user) =>
30
+ store.create(user.id, title, body, requestId)
31
+ ),
28
32
  updateNote: ({ id, body, revision }) =>
29
- Effect.flatMap(CurrentUser, (user) => store.update(user.id, id, body, revision)),
30
- deleteNote: ({ id }) => Effect.flatMap(CurrentUser, (user) => store.remove(user.id, id)),
31
- importNotes: ({ notes }) =>
33
+ Effect.flatMap(CurrentUser, (user) =>
34
+ store.update(user.id, id, body, revision)
35
+ ),
36
+ deleteNote: ({ id }) =>
37
+ Effect.flatMap(CurrentUser, (user) => store.remove(user.id, id)),
38
+ importNotes: ({ notes: imported }) =>
32
39
  Effect.map(
33
- Effect.flatMap(CurrentUser, (user) => store.createAll(user.id, notes)),
34
- (created) => ({ notes: created }),
40
+ Effect.flatMap(CurrentUser, (user) =>
41
+ store.createAll(user.id, imported)
42
+ ),
43
+ (created) => ({ notes: created })
35
44
  ),
36
45
  };
37
46
  }),
38
- { authorize },
47
+ { authorize }
39
48
  );