@gjermundgaraba/effect-actions 0.20.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 (321) hide show
  1. package/README.md +19 -41
  2. package/dist/Action.d.mts +140 -46
  3. package/dist/Action.mjs +87 -42
  4. package/dist/ActionCli.d.mts +69 -33
  5. package/dist/ActionCli.mjs +51 -32
  6. package/dist/ActionHttp.d.mts +69 -35
  7. package/dist/ActionHttp.mjs +75 -23
  8. package/dist/ActionMcp.d.mts +56 -22
  9. package/dist/ActionMcp.mjs +94 -69
  10. package/dist/ActionRpc.d.mts +222 -55
  11. package/dist/ActionRpc.mjs +299 -46
  12. package/dist/ActionToolkit.d.mts +22 -5
  13. package/dist/ActionToolkit.mjs +36 -12
  14. package/dist/Authentication.d.mts +42 -10
  15. package/dist/Authentication.mjs +37 -30
  16. package/dist/Event.d.mts +370 -0
  17. package/dist/Event.mjs +103 -0
  18. package/dist/Testing.mjs +1 -1
  19. package/dist/Webhook.d.mts +146 -0
  20. package/dist/Webhook.mjs +72 -0
  21. package/dist/authentication/provider.d.mts +35 -10
  22. package/dist/authentication/provider.mjs +1 -1
  23. package/dist/authentication/refusal.mjs +2 -2
  24. package/dist/cli/commands.d.mts +9 -5
  25. package/dist/cli/commands.mjs +84 -33
  26. package/dist/contract/ast.mjs +4 -1
  27. package/dist/contract/authorizer.d.mts +8 -0
  28. package/dist/contract/authorizer.mjs +20 -0
  29. package/dist/contract/call.d.mts +9 -6
  30. package/dist/contract/call.mjs +48 -6
  31. package/dist/contract/callback.d.mts +7 -0
  32. package/dist/contract/callback.mjs +27 -0
  33. package/dist/contract/caller.d.mts +13 -2
  34. package/dist/contract/caller.mjs +22 -3
  35. package/dist/contract/canonical.mjs +10 -0
  36. package/dist/contract/codec.d.mts +9 -0
  37. package/dist/contract/codec.mjs +9 -0
  38. package/dist/contract/codes.mjs +81 -0
  39. package/dist/contract/declared.mjs +25 -9
  40. package/dist/contract/digest.mjs +6 -0
  41. package/dist/contract/errors.d.mts +9 -3
  42. package/dist/contract/errors.mjs +1 -1
  43. package/dist/contract/feed.d.mts +127 -0
  44. package/dist/contract/feed.mjs +249 -0
  45. package/dist/contract/hub.d.mts +94 -0
  46. package/dist/contract/hub.mjs +133 -0
  47. package/dist/contract/idempotency.d.mts +47 -0
  48. package/dist/contract/idempotency.mjs +99 -0
  49. package/dist/contract/implementation.d.mts +73 -47
  50. package/dist/contract/implementation.mjs +92 -58
  51. package/dist/contract/input-check.d.mts +15 -0
  52. package/dist/contract/input-check.mjs +16 -0
  53. package/dist/contract/lease.mjs +28 -0
  54. package/dist/contract/listed.d.mts +4 -0
  55. package/dist/contract/listed.mjs +22 -0
  56. package/dist/contract/log.d.mts +41 -0
  57. package/dist/contract/log.mjs +144 -0
  58. package/dist/contract/names.mjs +14 -4
  59. package/dist/contract/occurrence.d.mts +29 -0
  60. package/dist/contract/occurrence.mjs +45 -0
  61. package/dist/contract/pages.d.mts +13 -0
  62. package/dist/contract/pages.mjs +39 -0
  63. package/dist/contract/progress.d.mts +26 -0
  64. package/dist/contract/progress.mjs +72 -0
  65. package/dist/contract/push.d.mts +20 -0
  66. package/dist/contract/push.mjs +96 -0
  67. package/dist/contract/receiving.d.mts +29 -0
  68. package/dist/contract/receiving.mjs +42 -0
  69. package/dist/contract/recent.mjs +12 -0
  70. package/dist/contract/reported.mjs +16 -0
  71. package/dist/contract/serving.mjs +63 -0
  72. package/dist/contract/status.mjs +14 -5
  73. package/dist/contract/stream.d.mts +34 -0
  74. package/dist/contract/stream.mjs +31 -0
  75. package/dist/contract/subscribe.d.mts +45 -0
  76. package/dist/contract/subscribe.mjs +50 -0
  77. package/dist/contract/transient.mjs +28 -0
  78. package/dist/contract/watch.d.mts +18 -0
  79. package/dist/contract/watch.mjs +105 -0
  80. package/dist/contract/webhooks.d.mts +42 -0
  81. package/dist/contract/webhooks.mjs +53 -0
  82. package/dist/http/client.d.mts +37 -12
  83. package/dist/http/client.mjs +51 -12
  84. package/dist/http/events.d.mts +56 -0
  85. package/dist/http/events.mjs +102 -0
  86. package/dist/http/placed.d.mts +36 -0
  87. package/dist/http/placed.mjs +71 -0
  88. package/dist/http/schema-errors.mjs +1 -1
  89. package/dist/http/streamed.d.mts +21 -0
  90. package/dist/http/streamed.mjs +94 -0
  91. package/dist/http/watch.mjs +50 -0
  92. package/dist/mcp/bridge.mjs +283 -0
  93. package/dist/mcp/client.d.mts +30 -97
  94. package/dist/mcp/client.mjs +59 -132
  95. package/dist/mcp/events.mjs +142 -0
  96. package/dist/mcp/progress.mjs +34 -0
  97. package/dist/mcp/protocol.mjs +39 -13
  98. package/dist/mcp/reply.d.mts +79 -0
  99. package/dist/mcp/reply.mjs +99 -0
  100. package/dist/mcp/request.d.mts +40 -0
  101. package/dist/mcp/request.mjs +51 -0
  102. package/dist/mcp/watch.mjs +115 -0
  103. package/dist/rpc/watch.mjs +39 -0
  104. package/dist/stdio/console.mjs +13 -5
  105. package/dist/testing/memory.d.mts +1 -1
  106. package/dist/testing/memory.mjs +3 -3
  107. package/dist/webhook/address.mjs +47 -0
  108. package/dist/webhook/attempt.mjs +65 -0
  109. package/dist/webhook/receiver.mjs +67 -0
  110. package/dist/webhook/signing.mjs +28 -0
  111. package/dist/webhook/stored.mjs +57 -0
  112. package/dist/webhook/subscriptions.mjs +342 -0
  113. package/docs/Action-failures.md +2 -2
  114. package/docs/Action-streams.md +115 -0
  115. package/docs/Action.md +58 -124
  116. package/docs/ActionCli-failures.md +1 -2
  117. package/docs/ActionCli.md +83 -88
  118. package/docs/ActionHttp-failures.md +3 -3
  119. package/docs/ActionHttp.md +90 -70
  120. package/docs/ActionMcp-failures.md +2 -1
  121. package/docs/ActionMcp.md +120 -92
  122. package/docs/ActionRpc-events.md +126 -0
  123. package/docs/ActionRpc-failures.md +1 -2
  124. package/docs/ActionRpc.md +108 -79
  125. package/docs/ActionToolkit-failures.md +1 -2
  126. package/docs/ActionToolkit.md +33 -90
  127. package/docs/Authentication-advanced.md +52 -60
  128. package/docs/Authentication-failures.md +5 -4
  129. package/docs/Authentication.md +23 -58
  130. package/docs/Event-failures.md +15 -0
  131. package/docs/Event.md +281 -0
  132. package/docs/README.md +51 -82
  133. package/docs/Testing-failures.md +1 -5
  134. package/docs/Testing.md +126 -86
  135. package/docs/Webhook-failures.md +8 -0
  136. package/docs/Webhook.md +319 -0
  137. package/docs/setup-failures.md +2 -3
  138. package/docs/setup.md +32 -52
  139. package/examples/README.md +12 -37
  140. package/examples/assistant/README.md +21 -26
  141. package/examples/assistant/src/agent.ts +66 -24
  142. package/examples/assistant/src/cli.ts +7 -4
  143. package/examples/assistant/src/commands.ts +9 -4
  144. package/examples/assistant/src/identity/Identity.ts +5 -4
  145. package/examples/assistant/src/identity/authorize.ts +13 -5
  146. package/examples/assistant/src/identity/handlers.ts +3 -2
  147. package/examples/assistant/src/identity/permissions.ts +9 -3
  148. package/examples/assistant/src/platform/tasks-file.ts +15 -11
  149. package/examples/assistant/src/scripted.ts +7 -4
  150. package/examples/assistant/src/tasks/Tasks.ts +20 -6
  151. package/examples/assistant/src/tasks/handlers.ts +9 -5
  152. package/examples/assistant/src/tasks/store.ts +38 -11
  153. package/examples/assistant/src/toolkit.ts +11 -7
  154. package/examples/assistant/test/commands.test.ts +53 -27
  155. package/examples/assistant/test/identity/identity.test.ts +27 -11
  156. package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
  157. package/examples/assistant/test/tasks/tasks.test.ts +62 -34
  158. package/examples/assistant/test/toolkit.test.ts +104 -75
  159. package/examples/build-events/README.md +83 -0
  160. package/examples/build-events/src/Api.ts +27 -0
  161. package/examples/build-events/src/Rpc.ts +26 -0
  162. package/examples/build-events/src/builds/Builds.ts +108 -0
  163. package/examples/build-events/src/builds/events.ts +54 -0
  164. package/examples/build-events/src/builds/handlers.ts +58 -0
  165. package/examples/build-events/src/builds/log.ts +30 -0
  166. package/examples/build-events/src/builds/status.ts +28 -0
  167. package/examples/build-events/src/builds/store.ts +130 -0
  168. package/examples/build-events/src/cli.ts +18 -0
  169. package/examples/build-events/src/commands.ts +32 -0
  170. package/examples/build-events/src/deploys/Deploys.ts +44 -0
  171. package/examples/build-events/src/deploys/approvers.ts +55 -0
  172. package/examples/build-events/src/deploys/events.ts +32 -0
  173. package/examples/build-events/src/deploys/handlers.ts +38 -0
  174. package/examples/build-events/src/identity/Identity.ts +29 -0
  175. package/examples/build-events/src/identity/members.ts +7 -0
  176. package/examples/build-events/src/identity/verifier.ts +22 -0
  177. package/examples/build-events/src/notifier/Notices.ts +26 -0
  178. package/examples/build-events/src/notifier/routes.ts +71 -0
  179. package/examples/build-events/src/notifier.ts +48 -0
  180. package/examples/build-events/src/routes.ts +71 -0
  181. package/examples/build-events/src/server.ts +62 -0
  182. package/examples/build-events/test/builds/events.test.ts +68 -0
  183. package/examples/build-events/test/builds/log.test.ts +71 -0
  184. package/examples/build-events/test/builds/progress.test.ts +57 -0
  185. package/examples/build-events/test/builds/status.test.ts +42 -0
  186. package/examples/build-events/test/commands.test.ts +103 -0
  187. package/examples/build-events/test/deploys/events.test.ts +52 -0
  188. package/examples/build-events/test/http.test.ts +95 -0
  189. package/examples/build-events/test/notifier.test.ts +80 -0
  190. package/examples/build-events/test/routes.test.ts +96 -0
  191. package/examples/build-events/test/rpc.test.ts +84 -0
  192. package/examples/build-events/test/support/as.ts +8 -0
  193. package/examples/build-events/test/support/memory.ts +43 -0
  194. package/examples/build-events/test/webhooks.test.ts +143 -0
  195. package/examples/kitchen-sink/README.md +178 -166
  196. package/examples/kitchen-sink/src/Actions.ts +11 -3
  197. package/examples/kitchen-sink/src/Api.ts +12 -6
  198. package/examples/kitchen-sink/src/Rpc.ts +7 -5
  199. package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
  200. package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
  201. package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
  202. package/examples/kitchen-sink/src/admin/grants.ts +5 -3
  203. package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
  204. package/examples/kitchen-sink/src/agent.ts +14 -6
  205. package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
  206. package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
  207. package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
  208. package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
  209. package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
  210. package/examples/kitchen-sink/src/attachments/download.ts +7 -6
  211. package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
  212. package/examples/kitchen-sink/src/attachments/store.ts +25 -18
  213. package/examples/kitchen-sink/src/browser.ts +10 -4
  214. package/examples/kitchen-sink/src/cli.ts +28 -12
  215. package/examples/kitchen-sink/src/config.ts +1 -1
  216. package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
  217. package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
  218. package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
  219. package/examples/kitchen-sink/src/identity/directory.ts +16 -7
  220. package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
  221. package/examples/kitchen-sink/src/identity/members.ts +7 -2
  222. package/examples/kitchen-sink/src/identity/quota.ts +23 -20
  223. package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
  224. package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
  225. package/examples/kitchen-sink/src/issues/events.ts +46 -0
  226. package/examples/kitchen-sink/src/issues/export.ts +35 -0
  227. package/examples/kitchen-sink/src/issues/guide.ts +4 -3
  228. package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
  229. package/examples/kitchen-sink/src/issues/store.ts +156 -67
  230. package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
  231. package/examples/kitchen-sink/src/memory.ts +11 -2
  232. package/examples/kitchen-sink/src/ops.ts +10 -5
  233. package/examples/kitchen-sink/src/request-policy.ts +21 -9
  234. package/examples/kitchen-sink/src/routes.ts +67 -27
  235. package/examples/kitchen-sink/src/sdk.ts +22 -12
  236. package/examples/kitchen-sink/src/seed.ts +15 -9
  237. package/examples/kitchen-sink/src/server.ts +24 -10
  238. package/examples/kitchen-sink/src/stdio.ts +9 -3
  239. package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
  240. package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
  241. package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
  242. package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
  243. package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
  244. package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
  245. package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
  246. package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
  247. package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
  248. package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
  249. package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
  250. package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
  251. package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
  252. package/examples/kitchen-sink/test/request-policy.test.ts +108 -47
  253. package/examples/kitchen-sink/test/routes.test.ts +102 -57
  254. package/examples/kitchen-sink/test/support/as.ts +9 -0
  255. package/examples/kitchen-sink/test/support/scripted.ts +6 -5
  256. package/examples/kitchen-sink/test/support/services.ts +17 -0
  257. package/examples/live-rpc/README.md +33 -35
  258. package/examples/live-rpc/src/Rpc.ts +9 -5
  259. package/examples/live-rpc/src/browser.ts +39 -11
  260. package/examples/live-rpc/src/chat/Chat.ts +30 -16
  261. package/examples/live-rpc/src/chat/authorize.ts +2 -1
  262. package/examples/live-rpc/src/chat/events.ts +74 -0
  263. package/examples/live-rpc/src/chat/handlers.ts +13 -5
  264. package/examples/live-rpc/src/chat/room.ts +9 -5
  265. package/examples/live-rpc/src/chat/store.ts +51 -14
  266. package/examples/live-rpc/src/client.ts +22 -5
  267. package/examples/live-rpc/src/identity/Identity.ts +3 -3
  268. package/examples/live-rpc/src/identity/verifier.ts +8 -4
  269. package/examples/live-rpc/src/page.ts +58 -12
  270. package/examples/live-rpc/src/routes.ts +14 -10
  271. package/examples/live-rpc/src/server.ts +15 -5
  272. package/examples/live-rpc/test/browser.test.ts +127 -50
  273. package/examples/live-rpc/test/chat/chat.test.ts +215 -23
  274. package/examples/live-rpc/test/routes.test.ts +130 -46
  275. package/examples/live-rpc/test/server.test.ts +94 -33
  276. package/examples/mcp-tools/README.md +24 -38
  277. package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
  278. package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
  279. package/examples/mcp-tools/src/printers/Printers.ts +18 -7
  280. package/examples/mcp-tools/src/printers/farm.ts +15 -5
  281. package/examples/mcp-tools/src/printers/handlers.ts +12 -6
  282. package/examples/mcp-tools/src/routes.ts +8 -6
  283. package/examples/mcp-tools/src/server.ts +12 -5
  284. package/examples/mcp-tools/src/stdio.ts +3 -2
  285. package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
  286. package/examples/mcp-tools/test/routes.test.ts +140 -81
  287. package/examples/notes-service/README.md +30 -45
  288. package/examples/notes-service/src/Api.ts +1 -0
  289. package/examples/notes-service/src/browser.ts +15 -14
  290. package/examples/notes-service/src/cli.ts +8 -3
  291. package/examples/notes-service/src/commands.ts +46 -20
  292. package/examples/notes-service/src/identity/Identity.ts +2 -2
  293. package/examples/notes-service/src/identity/authorize.ts +6 -2
  294. package/examples/notes-service/src/identity/handlers.ts +3 -2
  295. package/examples/notes-service/src/identity/users.ts +2 -1
  296. package/examples/notes-service/src/identity/verifier.ts +12 -6
  297. package/examples/notes-service/src/notes/Notes.ts +46 -22
  298. package/examples/notes-service/src/notes/handlers.ts +18 -9
  299. package/examples/notes-service/src/notes/store.ts +70 -43
  300. package/examples/notes-service/src/routes.ts +7 -5
  301. package/examples/notes-service/src/server.ts +12 -5
  302. package/examples/notes-service/src/status/Status.ts +1 -1
  303. package/examples/notes-service/src/status/handlers.ts +5 -3
  304. package/examples/notes-service/src/stdio.ts +7 -3
  305. package/examples/notes-service/src/worker.ts +6 -3
  306. package/examples/notes-service/test/commands.test.ts +164 -91
  307. package/examples/notes-service/test/notes/as.ts +3 -1
  308. package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
  309. package/examples/notes-service/test/notes/in-process.test.ts +26 -15
  310. package/examples/notes-service/test/notes/notes.test.ts +49 -31
  311. package/examples/notes-service/test/routes.test.ts +106 -71
  312. package/examples/notes-service/test/worker.test.ts +21 -13
  313. package/examples/quickstart/README.md +2 -6
  314. package/examples/quickstart/src/Api.ts +1 -0
  315. package/examples/quickstart/src/client.ts +6 -3
  316. package/examples/quickstart/src/greeting/Greeting.ts +1 -1
  317. package/examples/quickstart/src/greeting/handlers.ts +5 -2
  318. package/examples/quickstart/src/routes.ts +3 -2
  319. package/examples/quickstart/src/server.ts +6 -2
  320. package/examples/quickstart/test/routes.test.ts +3 -2
  321. package/package.json +10 -4
@@ -2,10 +2,11 @@ import { Config, Effect, Layer, Redacted, Schema } from "effect";
2
2
  import {
3
3
  HttpBody,
4
4
  HttpClient,
5
- type HttpClientError,
6
5
  HttpClientRequest,
7
6
  HttpClientResponse,
8
7
  } from "effect/http";
8
+ import type { HttpClientError } from "effect/http";
9
+
9
10
  import { PrintFarm } from "../printers/farm.js";
10
11
  import { Printer, PrintFarmError } from "../printers/Printers.js";
11
12
 
@@ -13,16 +14,24 @@ const fail = (message: string) => Effect.fail(new PrintFarmError({ message }));
13
14
 
14
15
  const unreadable = "The print farm sent an answer this server could not read.";
15
16
 
17
+ // An id is written into the path as it is: the contract admits only letters, digits,
18
+ // `_` and `-`, so it is always one segment. Encoding would not make other text safe:
19
+ // `encodeURIComponent` leaves `..` alone, and a URL parser then walks up the path.
20
+ const at = (id: string, rest = "") => `/api/printers/${id}${rest}`;
21
+
16
22
  // The farm answers a request it refuses with a status that is not 2xx and `{ "error": "..." }`.
17
23
  // Its reason is passed on to the model, which can act on "The printer is busy with a job".
18
24
  const refuseUnlessOk = HttpClientResponse.matchStatus({
19
25
  "2xx": (response) => Effect.succeed(response),
20
26
  orElse: (response) =>
21
- HttpClientResponse.schemaBodyJson(Schema.Struct({ error: Schema.String }))(response).pipe(
27
+ HttpClientResponse.schemaBodyJson(Schema.Struct({ error: Schema.String }))(
28
+ response
29
+ ).pipe(
22
30
  Effect.matchEffect({
23
- onFailure: () => fail(`The print farm refused the request (HTTP ${response.status}).`),
31
+ onFailure: () =>
32
+ fail(`The print farm refused the request (HTTP ${response.status}).`),
24
33
  onSuccess: ({ error }) => fail(`The print farm refused: ${error}`),
25
- }),
34
+ })
26
35
  ),
27
36
  });
28
37
 
@@ -30,24 +39,34 @@ const refuseUnlessOk = HttpClientResponse.matchStatus({
30
39
  // answer of another shape than expected, or none in time. Nothing of the HTTP client's own
31
40
  // errors leaves this file, so no tool can fail with something its contract does not declare.
32
41
  const asPrintFarmError = <A>(
33
- effect: Effect.Effect<A, PrintFarmError | HttpClientError.HttpClientError | Schema.SchemaError>,
42
+ effect: Effect.Effect<
43
+ A,
44
+ PrintFarmError | HttpClientError.HttpClientError | Schema.SchemaError
45
+ >
34
46
  ) =>
35
47
  effect.pipe(
36
- Effect.catchTag("HttpClientError", ({ response }) =>
37
- fail(response === undefined ? "The print farm could not be reached." : unreadable),
38
- ),
39
- Effect.catchTag("SchemaError", () => fail(unreadable)),
48
+ Effect.catchTags({
49
+ HttpClientError: ({ response }) =>
50
+ fail(
51
+ response === undefined
52
+ ? "The print farm could not be reached."
53
+ : unreadable
54
+ ),
55
+ SchemaError: () => fail(unreadable),
56
+ }),
40
57
  Effect.timeoutOrElse({
41
58
  duration: "30 seconds",
42
59
  orElse: () => fail("The print farm did not answer in time."),
43
- }),
60
+ })
44
61
  );
45
62
 
46
63
  const asImage = (response: HttpClientResponse.HttpClientResponse) =>
47
64
  Effect.gen(function* () {
48
65
  const mimeType = response.headers["content-type"];
49
66
 
50
- if (mimeType === undefined || !mimeType.startsWith("image/")) return yield* fail(unreadable);
67
+ if (mimeType === undefined || !mimeType.startsWith("image/")) {
68
+ return yield* fail(unreadable);
69
+ }
51
70
 
52
71
  return { data: new Uint8Array(yield* response.arrayBuffer), mimeType };
53
72
  });
@@ -67,7 +86,7 @@ export const printFarm = Layer.effect(
67
86
 
68
87
  const http = (yield* HttpClient.HttpClient).pipe(
69
88
  HttpClient.mapRequest(HttpClientRequest.prependUrl(url.href)),
70
- HttpClient.transformResponse(Effect.flatMap(refuseUnlessOk)),
89
+ HttpClient.transformResponse(Effect.flatMap(refuseUnlessOk))
71
90
  );
72
91
 
73
92
  // Signs in once, here, and every call below reuses the token, kept `Redacted` so no log
@@ -81,38 +100,49 @@ export const printFarm = Layer.effect(
81
100
  .pipe(
82
101
  Effect.flatMap(
83
102
  HttpClientResponse.schemaBodyJson(
84
- Schema.Struct({ token: Schema.RedactedFromValue(Schema.String) }),
85
- ),
103
+ Schema.Struct({ token: Schema.RedactedFromValue(Schema.String) })
104
+ )
86
105
  ),
87
- asPrintFarmError,
106
+ asPrintFarmError
88
107
  );
89
108
 
90
- yield* Effect.logInfo(`Signed in to the print farm at ${url.host} as ${user}.`);
91
-
92
- const api = HttpClient.mapRequest(http, HttpClientRequest.bearerToken(session.token));
109
+ yield* Effect.logInfo(
110
+ `Signed in to the print farm at ${url.host} as ${user}.`
111
+ );
93
112
 
94
- // An id is written into the path as it is: the contract admits only letters, digits,
95
- // `_` and `-`, so it is always one segment. Encoding would not make other text safe:
96
- // `encodeURIComponent` leaves `..` alone, and a URL parser then walks up the path.
97
- const at = (id: string, rest = "") => `/api/printers/${id}${rest}`;
113
+ const api = HttpClient.mapRequest(
114
+ http,
115
+ HttpClientRequest.bearerToken(session.token)
116
+ );
98
117
 
99
118
  const printer = (answer: ReturnType<typeof api.get>) =>
100
- answer.pipe(Effect.flatMap(HttpClientResponse.schemaBodyJson(Printer)), asPrintFarmError);
119
+ answer.pipe(
120
+ Effect.flatMap(HttpClientResponse.schemaBodyJson(Printer)),
121
+ asPrintFarmError
122
+ );
101
123
 
102
124
  return PrintFarm.of({
103
125
  printers: api.get("/api/printers").pipe(
104
126
  Effect.flatMap(
105
- HttpClientResponse.schemaBodyJson(Schema.Struct({ printers: Schema.Array(Printer) })),
127
+ HttpClientResponse.schemaBodyJson(
128
+ Schema.Struct({ printers: Schema.Array(Printer) })
129
+ )
106
130
  ),
107
131
  Effect.map(({ printers }) => printers),
108
- asPrintFarmError,
132
+ asPrintFarmError
109
133
  ),
110
134
  printer: (id) => printer(api.get(at(id))),
111
- snapshot: (id) => api.get(at(id, "/camera")).pipe(Effect.flatMap(asImage), asPrintFarmError),
135
+ snapshot: (id) =>
136
+ api
137
+ .get(at(id, "/camera"))
138
+ .pipe(Effect.flatMap(asImage), asPrintFarmError),
112
139
  start: (id, file) =>
113
- printer(api.post(at(id, "/job"), { body: HttpBody.jsonUnsafe({ file }) })),
114
- setPaused: (id, paused) => printer(api.post(at(id, paused ? "/pause" : "/resume"))),
140
+ printer(
141
+ api.post(at(id, "/job"), { body: HttpBody.jsonUnsafe({ file }) })
142
+ ),
143
+ setPaused: (id, paused) =>
144
+ printer(api.post(at(id, paused ? "/pause" : "/resume"))),
115
145
  cancel: (id) => printer(api.del(at(id, "/job"))),
116
146
  });
117
- }),
147
+ })
118
148
  );
@@ -1,5 +1,5 @@
1
- import { Schema } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import { Schema } from "effect";
3
3
 
4
4
  export const Printer = Schema.Struct({
5
5
  id: Schema.String,
@@ -8,8 +8,10 @@ export const Printer = Schema.Struct({
8
8
  job: Schema.NullOr(
9
9
  Schema.Struct({
10
10
  file: Schema.String,
11
- progress: Schema.Finite.annotate({ description: "Percent done, 0 to 100." }),
12
- }),
11
+ progress: Schema.Finite.annotate({
12
+ description: "Percent done, 0 to 100.",
13
+ }),
14
+ })
13
15
  ),
14
16
  });
15
17
 
@@ -18,7 +20,10 @@ export type Printer = typeof Printer.Type;
18
20
  // The one error every tool declares: the farm could not do what was asked. A model reads its
19
21
  // `message` and decides what to do next, so the message says what went wrong in plain words.
20
22
  // A handler failing with an error its action does not declare does not compile.
21
- export class PrintFarmError extends Schema.TaggedError<PrintFarmError>()("PrintFarmError", {
23
+ export class PrintFarmError extends Schema.TaggedError<
24
+ PrintFarmError,
25
+ { readonly brand: unique symbol }
26
+ >()("PrintFarmError", {
22
27
  message: Schema.String,
23
28
  }) {}
24
29
 
@@ -27,7 +32,9 @@ export class PrintFarmError extends Schema.TaggedError<PrintFarmError>()("PrintF
27
32
  // from a model. An id that does not match is refused as invalid input, on every surface,
28
33
  // before a handler runs, so `..` or `a/b` never reaches the farm. The `u` flag is what puts the
29
34
  // pattern in the tool's schema, where the model reads it.
30
- const PrinterId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]+$/u)).annotate({
35
+ const PrinterId = Schema.String.check(
36
+ Schema.isPattern(/^[A-Za-z0-9_-]+$/u)
37
+ ).annotate({
31
38
  description: "A printer's id, as list_printers gives it, such as mk4-1.",
32
39
  });
33
40
 
@@ -74,7 +81,9 @@ export const StartPrint = Action.make("start_print", {
74
81
  "named as the farm stores it. Refused when the printer is not idle.",
75
82
  input: {
76
83
  printer_id: PrinterId,
77
- file: Schema.String.annotate({ description: "A G-code file's name, such as bracket.gcode." }),
84
+ file: Schema.String.annotate({
85
+ description: "A G-code file's name, such as bracket.gcode.",
86
+ }),
78
87
  },
79
88
  success: Printer,
80
89
  readOnly: false,
@@ -89,7 +98,9 @@ export const SetPaused = Action.make("set_paused", {
89
98
  "pausing is the safe move while deciding what to do with a print that looks wrong.",
90
99
  input: {
91
100
  printer_id: PrinterId,
92
- paused: Schema.Boolean.annotate({ description: "true to pause the job, false to resume it." }),
101
+ paused: Schema.Boolean.annotate({
102
+ description: "true to pause the job, false to resume it.",
103
+ }),
93
104
  },
94
105
  success: Printer,
95
106
  readOnly: false,
@@ -1,5 +1,7 @@
1
- import { Context, type Effect } from "effect";
2
1
  import type * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import { Context } from "effect";
3
+ import type { Effect } from "effect";
4
+
3
5
  import type { Printer, PrintFarmError } from "./Printers.js";
4
6
 
5
7
  /**
@@ -10,11 +12,19 @@ import type { Printer, PrintFarmError } from "./Printers.js";
10
12
  export class PrintFarm extends Context.Service<
11
13
  PrintFarm,
12
14
  {
13
- readonly printers: Effect.Effect<ReadonlyArray<Printer>, PrintFarmError>;
15
+ readonly printers: Effect.Effect<readonly Printer[], PrintFarmError>;
14
16
  readonly printer: (id: string) => Effect.Effect<Printer, PrintFarmError>;
15
- readonly snapshot: (id: string) => Effect.Effect<typeof Action.Image.Type, PrintFarmError>;
16
- readonly start: (id: string, file: string) => Effect.Effect<Printer, PrintFarmError>;
17
- readonly setPaused: (id: string, paused: boolean) => Effect.Effect<Printer, PrintFarmError>;
17
+ readonly snapshot: (
18
+ id: string
19
+ ) => Effect.Effect<typeof Action.Image.Type, PrintFarmError>;
20
+ readonly start: (
21
+ id: string,
22
+ file: string
23
+ ) => Effect.Effect<Printer, PrintFarmError>;
24
+ readonly setPaused: (
25
+ id: string,
26
+ paused: boolean
27
+ ) => Effect.Effect<Printer, PrintFarmError>;
18
28
  readonly cancel: (id: string) => Effect.Effect<Printer, PrintFarmError>;
19
29
  }
20
30
  >()("mcp-tools/PrintFarm") {}
@@ -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 { PrintFarm } from "./farm.js";
4
5
  import { Printers } from "./Printers.js";
5
6
 
@@ -14,16 +15,21 @@ export const printers = Action.implement(
14
15
  const farm = yield* PrintFarm;
15
16
 
16
17
  return {
17
- list_printers: () => Effect.map(farm.printers, (all) => ({ printers: all })),
18
+ list_printers: () =>
19
+ Effect.map(farm.printers, (all) => ({ printers: all })),
18
20
  // Returned as `{ data, mimeType }` like any other field: the MCP surface makes the block.
19
21
  get_camera_snapshot: ({ printer_id }) =>
20
22
  Effect.all(
21
- { printer: farm.printer(printer_id), image: farm.snapshot(printer_id) },
22
- { concurrency: "unbounded" },
23
+ {
24
+ printer: farm.printer(printer_id),
25
+ image: farm.snapshot(printer_id),
26
+ },
27
+ { concurrency: "unbounded" }
23
28
  ),
24
29
  start_print: ({ printer_id, file }) => farm.start(printer_id, file),
25
- set_paused: ({ printer_id, paused }) => farm.setPaused(printer_id, paused),
30
+ set_paused: ({ printer_id, paused }) =>
31
+ farm.setPaused(printer_id, paused),
26
32
  cancel_print: ({ printer_id }) => farm.cancel(printer_id),
27
33
  };
28
- }),
34
+ })
29
35
  );
@@ -1,6 +1,7 @@
1
+ import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
1
2
  import { Config, Effect, Layer, Schema } from "effect";
2
3
  import { HttpRouter, HttpServerResponse } from "effect/http";
3
- import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
4
+
4
5
  import { serverInfo } from "./instructions.js";
5
6
  import { printers } from "./printers/handlers.js";
6
7
 
@@ -11,9 +12,10 @@ export const routes = Layer.unwrap(
11
12
  // A browser sends its page's `Origin`, and the endpoint refuses every origin not listed
12
13
  // here, so a web page a user happens to visit cannot call the tools. Other MCP clients
13
14
  // send none and pass. `ALLOWED_ORIGINS=https://a.example,https://b.example`; none by default.
14
- const allowedOrigins = yield* Config.Array(Schema.String, "ALLOWED_ORIGINS").pipe(
15
- Config.withDefault([]),
16
- );
15
+ const allowedOrigins = yield* Config.Array(
16
+ Schema.String,
17
+ "ALLOWED_ORIGINS"
18
+ ).pipe(Config.withDefault([]));
17
19
 
18
20
  // The Origin check only refuses. A browser also needs CORS: an answer to its preflight, and
19
21
  // headers on the endpoint's answers. `HttpRouter.cors` is global to the router and, given
@@ -39,7 +41,7 @@ export const routes = Layer.unwrap(
39
41
  return Layer.mergeAll(
40
42
  // No `authentication`: every tool is public, so the endpoint authenticates nothing.
41
43
  ActionMcp.layerHttp(printers, { ...serverInfo, allowedOrigins }),
42
- HttpRouter.add("GET", "/healthz", HttpServerResponse.text("ok")),
44
+ HttpRouter.add("GET", "/healthz", HttpServerResponse.text("ok"))
43
45
  ).pipe(Layer.provide(cors));
44
- }),
46
+ })
45
47
  );
@@ -1,7 +1,9 @@
1
1
  import { createServer } from "node:http";
2
+
2
3
  import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
3
4
  import { ByteSize, Config, Effect, Layer } from "effect";
4
5
  import { HttpRouter, HttpServerRequest } from "effect/http";
6
+
5
7
  import { fakeFarm } from "./platform/fake-farm.js";
6
8
  import { printFarm } from "./platform/print-farm.js";
7
9
  import { routes } from "./routes.js";
@@ -13,19 +15,24 @@ import { routes } from "./routes.js";
13
15
  // that need a caller are protected as docs/ActionMcp.md's "Protect tools" shows.
14
16
  const server = Layer.unwrap(
15
17
  Effect.gen(function* () {
16
- const host = yield* Config.String("HOST").pipe(Config.withDefault("127.0.0.1"));
18
+ const host = yield* Config.String("HOST").pipe(
19
+ Config.withDefault("127.0.0.1")
20
+ );
21
+
17
22
  const port = yield* Config.Port("PORT").pipe(Config.withDefault(3000));
18
23
 
19
24
  return HttpRouter.serve(routes).pipe(
20
25
  Layer.provide(NodeHttpServer.layer(createServer, { host, port })),
21
26
  // Request bodies have no size limit unless the host sets one. A tool call is a few
22
27
  // short fields, so anything larger is not one.
23
- Layer.provide(Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.kibibytes(64))),
28
+ Layer.provide(
29
+ Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.kibibytes(64))
30
+ ),
24
31
  // DEMO ONLY: `fakeFarm` answers the farm's API in memory. A real server provides
25
32
  // `FetchHttpClient.layer` in its place, and `printFarm` calls the farm at FARM_URL.
26
- Layer.provide(printFarm.pipe(Layer.provide(fakeFarm))),
33
+ Layer.provide(printFarm.pipe(Layer.provide(fakeFarm)))
27
34
  );
28
- }),
35
+ })
29
36
  );
30
37
 
31
- Layer.launch(server).pipe(NodeRuntime.runMain);
38
+ Layer.launch(server).pipe(NodeRuntime.runMain());
@@ -1,7 +1,8 @@
1
1
  import { NodeRuntime, NodeStdio } from "@effect/platform-node";
2
- import { Effect, Layer } from "effect";
3
2
  import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
4
3
  import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
4
+ import { Effect, Layer } from "effect";
5
+
5
6
  import { serverInfo } from "./instructions.js";
6
7
  import { fakeFarm } from "./platform/fake-farm.js";
7
8
  import { printFarm } from "./platform/print-farm.js";
@@ -22,5 +23,5 @@ ActionMcp.runStdio(printers, serverInfo).pipe(
22
23
  // stdout carries the protocol alone. Applied last, this sends every log to stderr, the
23
24
  // sign-in line `printFarm` logs included, and a startup failure too.
24
25
  ActionCli.logToStderr,
25
- NodeRuntime.runMain,
26
+ NodeRuntime.runMain()
26
27
  );