@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
@@ -1,30 +1,35 @@
1
1
  import { describe, expect, it } from "@effect/vitest";
2
- import { Effect, Layer } from "effect";
3
- import { RpcClient, RpcSerialization, RpcServer } from "effect/rpc";
4
2
  import * as Action from "@gjermundgaraba/effect-actions/Action";
5
3
  import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
4
+ import * as Event from "@gjermundgaraba/effect-actions/Event";
6
5
  import * as Testing from "@gjermundgaraba/effect-actions/Testing";
7
- import { roomHeader } from "../src/chat/Chat.js";
6
+ import { Effect, Fiber, Layer, Stream } from "effect";
7
+ import { RpcClient, RpcSerialization, RpcServer } from "effect/rpc";
8
+ import { TestClock } from "effect/testing";
9
+
10
+ import { MessagePosted, roomHeader } from "../src/chat/Chat.js";
8
11
  import { MessageStore } from "../src/chat/store.js";
9
12
  import { rpc } from "../src/routes.js";
10
13
  import { Rpc } from "../src/Rpc.js";
11
14
 
12
15
  // What the wire adds: the real rpcs, behind their real authentication and middleware,
13
16
  // answering in memory. The test gives them Effect's HTTP protocol, not the server's WebSocket,
14
- // because `Testing.layer` answers HTTP requests; no port is opened.
17
+ // because `Testing.layer` answers HTTP requests; no port is opened. NDJSON, not JSON: over
18
+ // HTTP, JSON sends a response once all its streams end, so a watch would never answer.
15
19
  const server = rpc.pipe(
16
20
  Layer.provide(RpcServer.layerProtocolHttp({ path: "/rpc" })),
17
- Layer.provide(RpcSerialization.layerJson),
21
+ Layer.provide(RpcSerialization.layerNdjson)
18
22
  );
19
23
 
20
24
  const served = RpcClient.layerProtocolHttp({ url: "/rpc" }).pipe(
21
- Layer.provide(RpcSerialization.layerJson),
25
+ Layer.provide(RpcSerialization.layerNdjson),
22
26
  Layer.provide(Testing.layer(server)),
23
- Layer.provide(MessageStore.layerMemory),
27
+ Layer.provide([MessageStore.layerMemory, Event.layerMemory])
24
28
  );
25
29
 
26
30
  // Each call carries its caller's token, so one client serves every caller of a test.
27
- const as = (token: string) => RpcClient.withHeaders({ authorization: `Bearer ${token}` });
31
+ const as = (token: string) =>
32
+ RpcClient.withHeaders({ authorization: `Bearer ${token}` });
28
33
 
29
34
  const inRoom = (room: string) => RpcClient.withHeaders({ [roomHeader]: room });
30
35
 
@@ -35,66 +40,145 @@ describe("the chat, over RPC", () => {
35
40
  Effect.gen(function* () {
36
41
  const client = yield* ActionRpc.client(Rpc);
37
42
  const signedOut = yield* Effect.flip(client.readMessages({ after: 0 }));
38
- const stranger = yield* Effect.flip(client.readMessages({ after: 0 }).pipe(as("mallory")));
39
43
 
40
- expect(yield* client.listRooms()).toEqual({ rooms: [] });
44
+ const stranger = yield* Effect.flip(
45
+ client.readMessages({ after: 0 }).pipe(as("mallory"))
46
+ );
47
+
48
+ expect(yield* client.listRooms()).toStrictEqual({ rooms: [] });
41
49
  expect(signedOut).toBeInstanceOf(Action.Unauthenticated);
42
50
  expect(signedOut.message).toBe("A bearer token is required.");
43
51
  expect(stranger).toBeInstanceOf(Action.Unauthenticated);
44
52
  expect(stranger.message).toBe("Unknown demo token.");
45
- }).pipe(Effect.provide(served)),
53
+ }).pipe(Effect.provide(served))
46
54
  );
47
55
 
48
- it.effect("authenticates each call by its own token: two callers on one client", () =>
49
- Effect.gen(function* () {
50
- const client = yield* ActionRpc.client(Rpc);
51
-
52
- yield* client.postMessage({ text: "Deploying at noon." }).pipe(as("ada"));
53
- yield* client.postMessage({ text: "Ready." }).pipe(as("grace"));
56
+ it.effect(
57
+ "authenticates each call by its own token: two callers on one client",
58
+ () =>
59
+ Effect.gen(function* () {
60
+ const client = yield* ActionRpc.client(Rpc);
54
61
 
55
- expect(yield* client.readMessages({ after: 0 }).pipe(as("grace"))).toEqual({
56
- messages: [
57
- { id: 1, author: "ada", text: "Deploying at noon." },
58
- { id: 2, author: "grace", text: "Ready." },
59
- ],
60
- });
61
- }).pipe(Effect.provide(served)),
62
+ yield* client
63
+ .postMessage({ text: "Deploying at noon." })
64
+ .pipe(as("ada"));
65
+ yield* client.postMessage({ text: "Ready." }).pipe(as("grace"));
66
+
67
+ expect(
68
+ yield* client.readMessages({ after: 0 }).pipe(as("grace"))
69
+ ).toStrictEqual({
70
+ messages: [
71
+ { id: 1, author: "ada", text: "Deploying at noon." },
72
+ { id: 2, author: "grace", text: "Ready." },
73
+ ],
74
+ });
75
+ }).pipe(Effect.provide(served))
62
76
  );
63
77
 
64
- it.effect("gives each call the room its header names, and the lobby when it names none", () =>
65
- Effect.gen(function* () {
66
- const client = yield* ActionRpc.client(Rpc);
67
-
68
- yield* client.postMessage({ text: "Disk is full." }).pipe(as("ada"), inRoom("ops"));
69
- yield* client.postMessage({ text: "Hello." }).pipe(as("ada"));
78
+ it.effect(
79
+ "gives each call the room its header names, and the lobby when it names none",
80
+ () =>
81
+ Effect.gen(function* () {
82
+ const client = yield* ActionRpc.client(Rpc);
70
83
 
71
- expect(yield* client.readMessages({ after: 0 }).pipe(as("ada"), inRoom("ops"))).toEqual({
72
- messages: [{ id: 1, author: "ada", text: "Disk is full." }],
73
- });
74
- expect(yield* client.readMessages({ after: 0 }).pipe(as("ada"), inRoom("lobby"))).toEqual({
75
- messages: [{ id: 1, author: "ada", text: "Hello." }],
76
- });
77
- }).pipe(Effect.provide(served)),
84
+ yield* client
85
+ .postMessage({ text: "Disk is full." })
86
+ .pipe(as("ada"), inRoom("ops"));
87
+ yield* client.postMessage({ text: "Hello." }).pipe(as("ada"));
88
+
89
+ expect(
90
+ yield* client
91
+ .readMessages({ after: 0 })
92
+ .pipe(as("ada"), inRoom("ops"))
93
+ ).toStrictEqual({
94
+ messages: [{ id: 1, author: "ada", text: "Disk is full." }],
95
+ });
96
+ expect(
97
+ yield* client
98
+ .readMessages({ after: 0 })
99
+ .pipe(as("ada"), inRoom("lobby"))
100
+ ).toStrictEqual({
101
+ messages: [{ id: 1, author: "ada", text: "Hello." }],
102
+ });
103
+ }).pipe(Effect.provide(served))
78
104
  );
79
105
 
80
106
  it.effect("refuses a member a room they have not joined, naming it", () =>
81
107
  Effect.gen(function* () {
82
108
  const client = yield* ActionRpc.client(Rpc);
83
- const read = client.readMessages({ after: 0 }).pipe(as("grace"), inRoom("ops"));
109
+
110
+ const read = client
111
+ .readMessages({ after: 0 })
112
+ .pipe(as("grace"), inRoom("ops"));
113
+
84
114
  const refused = yield* Effect.flip(read);
85
115
 
86
116
  expect(refused).toBeInstanceOf(Action.Forbidden);
87
117
  expect(refused.message).toBe("Not a member of ops.");
88
- }).pipe(Effect.provide(served)),
118
+ }).pipe(Effect.provide(served))
89
119
  );
90
120
 
91
- it.effect("serves a native RpcGroup: Effect's own client calls the same rpcs", () =>
92
- Effect.gen(function* () {
93
- // `Rpc.group` is what Effect's RPC tooling takes. Its methods take the payload as sent,
94
- // `{}` included; `ActionRpc.client` is the one to write an application with.
95
- const native = yield* RpcClient.make(Rpc.group);
121
+ it.effect(
122
+ "streams a room's posts to a member watching it, the room named by its header",
123
+ () =>
124
+ Effect.gen(function* () {
125
+ const client = yield* ActionRpc.client(Rpc);
126
+
127
+ const watching = yield* client
128
+ .watch(MessagePosted, {})
129
+ .pipe(
130
+ Stream.take(1),
131
+ Stream.runCollect,
132
+ as("grace"),
133
+ inRoom("lobby"),
134
+ Effect.forkChild
135
+ );
136
+
137
+ // Let the watch take its position through the in-memory socket before anyone posts.
138
+ yield* Effect.repeat(TestClock.adjust("0 millis"), { times: 10 });
139
+ yield* client
140
+ .postMessage({ text: "Disk is full." })
141
+ .pipe(as("ada"), inRoom("ops"));
142
+ yield* client
143
+ .postMessage({ text: "Ready." })
144
+ .pipe(as("ada"), inRoom("lobby"));
145
+
146
+ expect(yield* Fiber.join(watching)).toMatchObject([
147
+ {
148
+ _tag: "Occurrence",
149
+ data: { id: 1, author: "ada", text: "Ready." },
150
+ },
151
+ ]);
152
+ }).pipe(Effect.provide(served))
153
+ );
154
+
155
+ it.effect(
156
+ "refuses a watch of a room the member has not joined, as it starts",
157
+ () =>
158
+ Effect.gen(function* () {
159
+ const client = yield* ActionRpc.client(Rpc);
160
+
161
+ const refused = yield* Effect.flip(
162
+ Stream.runDrain(client.watch(MessagePosted, {})).pipe(
163
+ as("grace"),
164
+ inRoom("ops")
165
+ )
166
+ );
167
+
168
+ expect(refused).toBeInstanceOf(Action.Forbidden);
169
+ expect(refused).toMatchObject({ message: "Not a member of ops." });
170
+ }).pipe(Effect.provide(served))
171
+ );
172
+
173
+ it.effect(
174
+ "serves a native RpcGroup: Effect's own client calls the same rpcs",
175
+ () =>
176
+ Effect.gen(function* () {
177
+ // `Rpc.group` is what Effect's RPC tooling takes. Its methods take the payload as sent,
178
+ // `{}` included; `ActionRpc.client` is the one to write an application with.
179
+ const native = yield* RpcClient.make(Rpc.group);
96
180
 
97
- expect(yield* native.listRooms({})).toEqual({ rooms: [] });
98
- }).pipe(Effect.provide(served)),
181
+ expect(yield* native.listRooms({})).toStrictEqual({ rooms: [] });
182
+ }).pipe(Effect.provide(served))
99
183
  );
100
184
  });
@@ -1,11 +1,14 @@
1
1
  import { createServer } from "node:http";
2
+
2
3
  import { NodeHttpServer } from "@effect/platform-node";
3
4
  import { describe, expect, it } from "@effect/vitest";
4
- import { Effect, Layer } from "effect";
5
+ import * as Event from "@gjermundgaraba/effect-actions/Event";
6
+ import { Deferred, Effect, Layer } from "effect";
5
7
  import { HttpRouter, HttpServer } from "effect/http";
6
8
  import { RpcSerialization, RpcServer } from "effect/rpc";
7
- import { connect } from "../src/page.js";
9
+
8
10
  import { MessageStore } from "../src/chat/store.js";
11
+ import { connect } from "../src/page.js";
9
12
  import { rpc } from "../src/routes.js";
10
13
 
11
14
  // What the socket adds, which no in-memory test reaches: the upgrade, the page's `connect(url)`
@@ -14,18 +17,25 @@ import { rpc } from "../src/routes.js";
14
17
  // by side. The server closes with the test's scope.
15
18
  const socket = rpc.pipe(
16
19
  Layer.provide(RpcServer.layerProtocolWebsocket({ path: "/rpc" })),
17
- Layer.provide(RpcSerialization.layerJson),
20
+ Layer.provide(RpcSerialization.layerJson)
18
21
  );
19
22
 
20
23
  // server.ts allows 64 KiB. A bound this small is passed by a message the contract accepts.
21
24
  const maxPayload = 256;
22
25
 
23
- const listening = HttpRouter.serve(socket, { disableLogger: true, disableListenLog: true }).pipe(
26
+ const listening = HttpRouter.serve(socket, {
27
+ disableLogger: true,
28
+ disableListenLog: true,
29
+ }).pipe(
24
30
  // `provideMerge` keeps `HttpServer` for the test, which reads the port from it.
25
31
  Layer.provideMerge(
26
- NodeHttpServer.layer(createServer, { host: "127.0.0.1", port: 0, websocket: { maxPayload } }),
32
+ NodeHttpServer.layer(createServer, {
33
+ host: "127.0.0.1",
34
+ port: 0,
35
+ websocket: { maxPayload },
36
+ })
27
37
  ),
28
- Layer.provide(MessageStore.layerMemory),
38
+ Layer.provide([MessageStore.layerMemory, Event.layerMemory])
29
39
  );
30
40
 
31
41
  // The page's client, on Node's global `WebSocket`. Each test closes it with `Effect.scoped`,
@@ -36,40 +46,91 @@ const chat = Effect.gen(function* () {
36
46
 
37
47
  return yield* Effect.acquireRelease(
38
48
  Effect.sync(() => connect(`${url}/rpc`)),
39
- (page) => Effect.promise(() => page.close()),
49
+ (page) =>
50
+ Effect.promise(async () => {
51
+ await page.close();
52
+ })
40
53
  );
41
54
  });
42
55
 
43
56
  describe("the chat, over a WebSocket", () => {
44
- it.effect("answers the page's client on a listening port, each call as its token names", () =>
45
- Effect.gen(function* () {
46
- const page = yield* chat;
47
-
48
- const posted = yield* Effect.promise(() =>
49
- page.room("lobby", "ada").post("Deploying at noon."),
50
- );
51
-
52
- expect(posted).toEqual({ id: 1, author: "ada", text: "Deploying at noon." });
53
- expect(yield* Effect.promise(() => page.room("lobby", "grace").read(0))).toEqual({
54
- access: "member",
55
- messages: [posted],
56
- });
57
- }).pipe(Effect.scoped, Effect.provide(listening)),
57
+ it.effect(
58
+ "answers the page's client on a listening port, each call as its token names",
59
+ () =>
60
+ Effect.gen(function* () {
61
+ const page = yield* chat;
62
+
63
+ const posted = yield* Effect.promise(
64
+ async () => await page.room("lobby", "ada").post("Deploying at noon.")
65
+ );
66
+
67
+ expect(posted).toStrictEqual({
68
+ id: 1,
69
+ author: "ada",
70
+ text: "Deploying at noon.",
71
+ });
72
+ expect(
73
+ yield* Effect.promise(
74
+ async () => await page.room("lobby", "grace").read(0)
75
+ )
76
+ ).toStrictEqual({
77
+ access: "member",
78
+ messages: [posted],
79
+ });
80
+ }).pipe(Effect.scoped, Effect.provide(listening))
81
+ );
82
+
83
+ it.effect(
84
+ "delivers a post to a page watching the room on the same socket, until it stops",
85
+ () =>
86
+ Effect.gen(function* () {
87
+ const page = yield* chat;
88
+ const received = yield* Deferred.make<string>();
89
+ const stopped = yield* Deferred.make<string>();
90
+
91
+ // From the room's start, id 0, so the post reaches it however soon after the watch it is
92
+ // sent.
93
+ const stop = page.room("lobby", "grace").watch(
94
+ 0,
95
+ (message) => {
96
+ if (message._tag === "Occurrence") {
97
+ Deferred.doneUnsafe(received, Effect.succeed(message.data.text));
98
+ }
99
+ },
100
+ (why) => {
101
+ Deferred.doneUnsafe(stopped, Effect.succeed(why));
102
+ }
103
+ );
104
+
105
+ yield* Effect.promise(
106
+ async () => await page.room("lobby", "ada").post("Deploying at noon.")
107
+ );
108
+
109
+ expect(yield* Deferred.await(received)).toBe("Deploying at noon.");
110
+
111
+ stop();
112
+
113
+ expect(yield* Deferred.await(stopped)).toBe("stopped");
114
+ }).pipe(Effect.scoped, Effect.provide(listening))
58
115
  );
59
116
 
60
- it.effect("closes the socket on a message over the bound, failing the call that sent it", () =>
61
- Effect.gen(function* () {
62
- const page = yield* chat;
63
- const ada = page.room("lobby", "ada");
117
+ it.effect(
118
+ "closes the socket on a message over the bound, failing the call that sent it",
119
+ () =>
120
+ Effect.gen(function* () {
121
+ const page = yield* chat;
122
+ const ada = page.room("lobby", "ada");
64
123
 
65
- yield* Effect.promise(() => ada.post("Short."));
124
+ yield* Effect.promise(async () => await ada.post("Short."));
66
125
 
67
- // 400 characters are a text the contract accepts and a message over 256 bytes. The server
68
- // reads none of it: it closes the socket with 1009, "message too big", and the call fails
69
- // with Effect's `RpcClientError`, its reason the socket's close, which no action declares.
70
- yield* Effect.promise(() =>
71
- expect(ada.post("x".repeat(400))).rejects.toMatchObject({ reason: { code: 1009 } }),
72
- );
73
- }).pipe(Effect.scoped, Effect.provide(listening)),
126
+ // 400 characters are a text the contract accepts and a message over 256 bytes. The server
127
+ // reads none of it: it closes the socket with 1009, "message too big", and the call fails
128
+ // with Effect's `RpcClientError`, its reason the socket's close, which no action declares.
129
+ yield* Effect.promise(async () => {
130
+ await expect(ada.post("x".repeat(400))).rejects.toMatchObject({
131
+ reason: { code: 1009 },
132
+ });
133
+ });
134
+ }).pipe(Effect.scoped, Effect.provide(listening))
74
135
  );
75
136
  });
@@ -1,14 +1,10 @@
1
1
  # mcp-tools
2
2
 
3
- An MCP server whose tools wrap one upstream HTTP API: here, a 3D print farm's. It has no
4
- authentication of its own. It signs in to the farm once, with its own account, and whoever can
5
- reach it may call every tool, so it runs as a local subprocess of an agent or on a network only
6
- trusted clients are on.
3
+ An MCP server whose tools wrap one upstream HTTP API: here, a 3D print farm's. It has no authentication of its own. It signs in to the farm once, with its own account, and whoever can reach it may call every tool, so it runs as a local subprocess of an agent or on a network only trusted clients are on.
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
  export FARM_URL=https://farm.example.test FARM_USER=demo FARM_PASSWORD=demo
@@ -23,9 +19,7 @@ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":
23
19
  vp test examples/mcp-tools
24
20
  ```
25
21
 
26
- The farm is a fake, answered in memory, so any URL, user and password work. `ALLOWED_ORIGINS` lists the browser origins the HTTP
27
- endpoint accepts and answers with CORS headers, comma-separated; `HOST` is the interface it
28
- listens on.
22
+ The farm is a fake, answered in memory, so any URL, user and password work. `ALLOWED_ORIGINS` lists the browser origins the HTTP endpoint accepts and answers with CORS headers, comma-separated; `HOST` is the interface it listens on.
29
23
 
30
24
  ## Connect a client
31
25
 
@@ -39,10 +33,7 @@ claude mcp add print-farm \
39
33
  -- "$PWD/node_modules/.bin/tsx" "$PWD/examples/mcp-tools/src/stdio.ts"
40
34
  ```
41
35
 
42
- `node --import tsx` finds `tsx` from the directory the process starts in, which the host
43
- chooses. So the line names both files by their full path, which `$PWD` gives it when run from
44
- the repository root; `tsx` runs the `node` on the host's `PATH`. Claude Desktop takes the same
45
- command as an entry of `mcpServers` in its `claude_desktop_config.json`, the paths written out:
36
+ `node --import tsx` finds `tsx` from the directory the process starts in, which the host chooses. So the line names both files by their full path, which `$PWD` gives it when run from the repository root; `tsx` runs the `node` on the host's `PATH`. Claude Desktop takes the same command as an entry of `mcpServers` in its `claude_desktop_config.json`, the paths written out:
46
37
 
47
38
  ```json
48
39
  {
@@ -60,8 +51,7 @@ command as an entry of `mcpServers` in its `claude_desktop_config.json`, the pat
60
51
  }
61
52
  ```
62
53
 
63
- By hand, a tool is called with one request, which states its protocol revision, its method
64
- and its tool's name twice: in the body and in the three `MCP-` headers.
54
+ By hand, a tool is called with one request, which states its protocol revision, its method and its tool's name twice: in the body and in the three `MCP-` headers.
65
55
 
66
56
  ```sh
67
57
  curl http://127.0.0.1:3000/mcp \
@@ -73,11 +63,7 @@ curl http://127.0.0.1:3000/mcp \
73
63
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_printers","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
74
64
  ```
75
65
 
76
- It answers `{"jsonrpc":"2.0","id":1,"result":{…,"structuredContent":{"printers":[…]},"isError":false}}`.
77
- Every header is needed, as `ActionMcp.request` builds it, and
78
- [ActionMcp-failures.md](../../docs/ActionMcp-failures.md) lists what a missing one answers.
79
- `tools/list` takes the same request with that method, no `MCP-Name` header,
80
- and no `name` or `arguments`.
66
+ It answers `{"jsonrpc":"2.0","id":1,"result":{…,"structuredContent":{"printers":[…]},"isError":false}}`. Every header is needed, as `ActionMcp.request` builds it, and [ActionMcp-failures.md](../../docs/ActionMcp-failures.md) lists what a missing one answers. `tools/list` takes the same request with that method, no `MCP-Name` header, and no `name` or `arguments`.
81
67
 
82
68
  ## Layout
83
69
 
@@ -101,21 +87,21 @@ test/
101
87
 
102
88
  ## Where to look
103
89
 
104
- | To see | Read |
105
- | ----------------------------------------------------------------------- | -------------------------------- |
106
- | Public tools described for a model, with field descriptions | `src/printers/Printers.ts` |
107
- | `readOnly`, `title`, `idempotentHint`, and destructive as MCP's default | `src/printers/Printers.ts` |
108
- | One declared error shared by every tool | `src/printers/Printers.ts` |
109
- | An input a model cannot use to reach another URL | `src/printers/Printers.ts` |
110
- | A tool answering an image beside other fields | `src/printers/Printers.ts` |
111
- | An upstream client reading `Config`, signing in once | `src/platform/print-farm.ts` |
112
- | An upstream's failures mapped to the declared error | `src/platform/print-farm.ts` |
113
- | A builder yielding a service, and handlers that only delegate | `src/printers/handlers.ts` |
114
- | Server `instructions`, shared by both transports | `src/instructions.ts` |
115
- | Allowed origins from `Config`, their CORS, and `/healthz` | `src/routes.ts` |
116
- | A request body limit, and where an unauthenticated server binds | `src/server.ts` |
117
- | A stdio server whose logs go to stderr | `src/stdio.ts` |
118
- | A fake upstream provided as a Layer, to a test or an entrypoint | `src/platform/fake-farm.ts` |
119
- | Testing tools and the upstream's failure messages in process | `test/printers/printers.test.ts` |
120
- | Testing listed hints, an image and the origin check in memory | `test/routes.test.ts` |
121
- | Testing CORS for one configured origin and for several | `test/routes.test.ts` |
90
+ | To see | Read |
91
+ | --- | --- |
92
+ | Public tools described for a model, with field descriptions | `src/printers/Printers.ts` |
93
+ | `readOnly`, `title`, `idempotentHint`, and destructive as MCP's default | `src/printers/Printers.ts` |
94
+ | One declared error shared by every tool | `src/printers/Printers.ts` |
95
+ | An input a model cannot use to reach another URL | `src/printers/Printers.ts` |
96
+ | A tool answering an image beside other fields | `src/printers/Printers.ts` |
97
+ | An upstream client reading `Config`, signing in once | `src/platform/print-farm.ts` |
98
+ | An upstream's failures mapped to the declared error | `src/platform/print-farm.ts` |
99
+ | A builder yielding a service, and handlers that only delegate | `src/printers/handlers.ts` |
100
+ | Server `instructions`, shared by both transports | `src/instructions.ts` |
101
+ | Allowed origins from `Config`, their CORS, and `/healthz` | `src/routes.ts` |
102
+ | A request body limit, and where an unauthenticated server binds | `src/server.ts` |
103
+ | A stdio server whose logs go to stderr | `src/stdio.ts` |
104
+ | A fake upstream provided as a Layer, to a test or an entrypoint | `src/platform/fake-farm.ts` |
105
+ | Testing tools and the upstream's failure messages in process | `test/printers/printers.test.ts` |
106
+ | Testing listed hints, an image and the origin check in memory | `test/routes.test.ts` |
107
+ | Testing CORS for one configured origin and for several | `test/routes.test.ts` |
@@ -1,6 +1,8 @@
1
1
  import { Buffer } from "node:buffer";
2
+
2
3
  import { Effect, Layer, Option, Schema } from "effect";
3
4
  import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
5
+
4
6
  import type { Printer } from "../printers/Printers.js";
5
7
 
6
8
  // DEMO ONLY: the farm's HTTP API answered in memory, so the example and its tests run without
@@ -12,19 +14,25 @@ import type { Printer } from "../printers/Printers.js";
12
14
  export const picture = Uint8Array.from(
13
15
  Buffer.from(
14
16
  "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
15
- "base64",
16
- ),
17
+ "base64"
18
+ )
17
19
  );
18
20
 
19
21
  const token = "demo-session";
20
22
 
21
23
  const Login = Schema.fromJsonString(
22
- Schema.Struct({ user: Schema.NonEmptyString, password: Schema.NonEmptyString }),
24
+ Schema.Struct({
25
+ user: Schema.NonEmptyString,
26
+ password: Schema.NonEmptyString,
27
+ })
23
28
  );
24
29
 
25
- const Job = Schema.fromJsonString(Schema.Struct({ file: Schema.NonEmptyString }));
30
+ const Job = Schema.fromJsonString(
31
+ Schema.Struct({ file: Schema.NonEmptyString })
32
+ );
26
33
 
27
- const refuse = (status: number, error: string) => Response.json({ error }, { status });
34
+ const refuse = (status: number, error: string) =>
35
+ Response.json({ error }, { status });
28
36
 
29
37
  export const fakeFarm = Layer.sync(HttpClient.HttpClient, () => {
30
38
  const printers = new Map<string, Printer>([
@@ -37,33 +45,46 @@ export const fakeFarm = Layer.sync(HttpClient.HttpClient, () => {
37
45
  job: { file: "bracket.gcode", progress: 62 },
38
46
  },
39
47
  ],
40
- ["mk4-2", { id: "mk4-2", name: "MK4 by the window", state: "idle", job: null }],
41
- ["mini-1", { id: "mini-1", name: "Mini on the shelf", state: "offline", job: null }],
48
+ [
49
+ "mk4-2",
50
+ { id: "mk4-2", name: "MK4 by the window", state: "idle", job: null },
51
+ ],
52
+ [
53
+ "mini-1",
54
+ { id: "mini-1", name: "Mini on the shelf", state: "offline", job: null },
55
+ ],
42
56
  ]);
43
57
 
44
58
  const answer = (
45
59
  method: string,
46
60
  path: string,
47
61
  authorization: string | undefined,
48
- body: string,
62
+ body: string
49
63
  ): Response => {
50
64
  if (method === "POST" && path === "/api/login") {
51
- return Option.match(Schema.decodeUnknownOption(Login)(body), {
65
+ return Option.match(Schema.decodeOption(Login)(body), {
52
66
  onNone: () => refuse(400, "Give a user and a password."),
53
67
  onSome: () => Response.json({ token }),
54
68
  });
55
69
  }
56
70
 
57
- if (authorization !== `Bearer ${token}`) return refuse(401, "Sign in first.");
71
+ if (authorization !== `Bearer ${token}`) {
72
+ return refuse(401, "Sign in first.");
73
+ }
58
74
 
59
75
  if (method === "GET" && path === "/api/printers") {
60
- return Response.json({ printers: Array.from(printers.values()) });
76
+ return Response.json({ printers: [...printers.values()] });
61
77
  }
62
78
 
63
- const [id = "", action = ""] = path.replace("/api/printers/", "").split("/");
79
+ const [id = "", action = ""] = path
80
+ .replace("/api/printers/", "")
81
+ .split("/");
82
+
64
83
  const printer = printers.get(id);
65
84
 
66
- if (printer === undefined) return refuse(404, `No printer has the id ${id}.`);
85
+ if (printer === undefined) {
86
+ return refuse(404, `No printer has the id ${id}.`);
87
+ }
67
88
 
68
89
  const becomes = (state: Printer["state"], job: Printer["job"]) => {
69
90
  const changed = { ...printer, state, job };
@@ -78,38 +99,56 @@ export const fakeFarm = Layer.sync(HttpClient.HttpClient, () => {
78
99
  }
79
100
 
80
101
  switch (`${method} ${action}`) {
81
- case "GET ":
102
+ case "GET ": {
82
103
  return Response.json(printer);
83
- case "GET camera":
84
- return new Response(picture, { headers: { "content-type": "image/png" } });
85
- case "POST job":
104
+ }
105
+
106
+ case "GET camera": {
107
+ return new Response(picture, {
108
+ headers: { "content-type": "image/png" },
109
+ });
110
+ }
111
+
112
+ case "POST job": {
86
113
  return printer.job === null
87
- ? Option.match(Schema.decodeUnknownOption(Job)(body), {
114
+ ? Option.match(Schema.decodeOption(Job)(body), {
88
115
  onNone: () => refuse(400, "Name the file to print."),
89
116
  onSome: ({ file }) => becomes("printing", { file, progress: 0 }),
90
117
  })
91
118
  : refuse(409, `${printer.name} is busy with ${printer.job.file}.`);
119
+ }
120
+
92
121
  case "POST pause":
93
- case "POST resume":
122
+ case "POST resume": {
94
123
  return printer.job === null
95
124
  ? refuse(409, `${printer.name} has no job.`)
96
125
  : becomes(action === "pause" ? "paused" : "printing", printer.job);
97
- case "DELETE job":
126
+ }
127
+
128
+ case "DELETE job": {
98
129
  return becomes("idle", null);
99
- default:
130
+ }
131
+
132
+ default: {
100
133
  return refuse(404, `No such route: ${method} ${path}.`);
134
+ }
101
135
  }
102
136
  };
103
137
 
104
138
  return HttpClient.make((request, url) =>
105
139
  Effect.gen(function* () {
106
140
  const sent = yield* Effect.orDie(HttpClientRequest.toWeb(request));
107
- const body = yield* Effect.promise(() => sent.text());
141
+ const body = yield* Effect.promise(async () => await sent.text());
108
142
 
109
143
  return HttpClientResponse.fromWeb(
110
144
  request,
111
- answer(request.method, url.pathname, request.headers["authorization"], body),
145
+ answer(
146
+ request.method,
147
+ url.pathname,
148
+ request.headers["authorization"],
149
+ body
150
+ )
112
151
  );
113
- }),
152
+ })
114
153
  );
115
154
  });