@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,17 +1,21 @@
1
- import { Effect, Redacted } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
3
2
  import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
4
- import { type KeyOperator, OperatorKey } from "./Admin.js";
3
+ import { Effect, Redacted } from "effect";
4
+
5
+ import { OperatorKey } from "./Admin.js";
6
+ import type { KeyOperator } from "./Admin.js";
5
7
  import { keys } from "./keys.js";
6
8
 
7
9
  // Typed to return a `KeyOperator`, never the `Operator` union (Admin.ts says why). The verifier
8
10
  // receives what its scheme decodes, the header's value, and needs no startup service, so it is
9
11
  // passed as it is.
10
12
  const verify = (
11
- key: Redacted.Redacted<string>,
13
+ key: Redacted.Redacted
12
14
  ): Effect.Effect<KeyOperator, Action.Unauthenticated> =>
13
15
  Effect.fromNullishOr(keys.get(Redacted.value(key))).pipe(
14
- Effect.mapError(() => new Action.Unauthenticated({ message: "Unknown operator key." })),
16
+ Effect.mapError(
17
+ () => new Action.Unauthenticated({ message: "Unknown operator key." })
18
+ )
15
19
  );
16
20
 
17
21
  // Server only: `OperatorKey`'s provider, which the admin port's routes require.
@@ -1,7 +1,9 @@
1
- import { Effect } from "effect";
2
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import { Effect } from "effect";
3
+
3
4
  import { Operator } from "./Admin.js";
4
- import { type Grant, Grants } from "./grants.js";
5
+ import { Grants } from "./grants.js";
6
+ import type { Grant } from "./grants.js";
5
7
 
6
8
  // An authorizer built from a startup service, as a handler builder builds handlers: the
7
9
  // Effect yields `Grants` once, when the implementation is built, and the function it returns
@@ -14,13 +16,19 @@ export const authorize = Effect.gen(function* () {
14
16
  const operator = yield* Operator;
15
17
 
16
18
  // The one rule admits the console's operator too: no surface has a bypass of its own.
17
- if (operator.trusted) return;
19
+ if (operator.trusted) {
20
+ return;
21
+ }
18
22
 
19
- const grant: Grant = action.readOnly ? "workspaces:read" : "workspaces:suspend";
23
+ const grant: Grant = action.readOnly
24
+ ? "workspaces:read"
25
+ : "workspaces:suspend";
20
26
 
21
27
  // No `scopes`: a key cannot be re-authorized, so this is a plain 403.
22
28
  if (!(yield* grants.of(operator.id)).includes(grant)) {
23
- return yield* new Action.Forbidden({ message: `Requires the ${grant} grant.` });
29
+ return yield* new Action.Forbidden({
30
+ message: `Requires the ${grant} grant.`,
31
+ });
24
32
  }
25
33
  });
26
34
  });
@@ -8,15 +8,17 @@ export type Grant = "workspaces:read" | "workspaces:suspend";
8
8
  */
9
9
  export class Grants extends Context.Service<
10
10
  Grants,
11
- { readonly of: (operator: string) => Effect.Effect<ReadonlyArray<Grant>> }
11
+ { readonly of: (operator: string) => Effect.Effect<readonly Grant[]> }
12
12
  >()("tracker/Grants") {
13
13
  // DEMO ONLY: fixed grants. A real application reads them from its database.
14
14
  static readonly layerDemo = Layer.sync(Grants, () => {
15
- const grants = new Map<string, ReadonlyArray<Grant>>([
15
+ const grants = new Map<string, readonly Grant[]>([
16
16
  ["ops", ["workspaces:read", "workspaces:suspend"]],
17
17
  ["support", ["workspaces:read"]],
18
18
  ]);
19
19
 
20
- return Grants.of({ of: (operator) => Effect.sync(() => grants.get(operator) ?? []) });
20
+ return Grants.of({
21
+ of: (operator) => Effect.sync(() => grants.get(operator) ?? []),
22
+ });
21
23
  });
22
24
  }
@@ -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 { WorkspaceRegistry } from "../workspaces/registry.js";
4
5
  import { Admin } from "./Admin.js";
5
6
  import { authorize } from "./authorize.js";
@@ -12,10 +13,13 @@ export const admin = Action.implement(
12
13
  const registry = yield* WorkspaceRegistry;
13
14
 
14
15
  return {
15
- listWorkspaces: () => Effect.map(registry.list, (workspaces) => ({ workspaces })),
16
- suspendWorkspace: ({ workspace }) => registry.setSuspended(workspace, true),
17
- resumeWorkspace: ({ workspace }) => registry.setSuspended(workspace, false),
16
+ listWorkspaces: () =>
17
+ Effect.map(registry.list, (workspaces) => ({ workspaces })),
18
+ suspendWorkspace: ({ workspace }) =>
19
+ registry.setSuspended(workspace, true),
20
+ resumeWorkspace: ({ workspace }) =>
21
+ registry.setSuspended(workspace, false),
18
22
  };
19
23
  }),
20
- { authorize },
24
+ { authorize }
21
25
  );
@@ -1,6 +1,7 @@
1
1
  import { NodeRuntime } from "@effect/platform-node";
2
- import { Console, Effect, Layer } from "effect";
3
2
  import * as Action from "@gjermundgaraba/effect-actions/Action";
3
+ import { Console, Effect, Layer } from "effect";
4
+
4
5
  import { demoModel } from "./assistant/demo-model.js";
5
6
  import { CurrentMember } from "./identity/Identity.js";
6
7
  import { members } from "./identity/members.js";
@@ -18,7 +19,10 @@ const program = Effect.gen(function* () {
18
19
  // `authorize` and handler, with no transport. The store is memory, so file an issue first.
19
20
  const tracker = yield* Action.client(issues);
20
21
 
21
- yield* tracker.createIssue({ title: "Crash on save", body: "Stack trace attached." });
22
+ yield* tracker.createIssue({
23
+ title: "Crash on save",
24
+ body: "Stack trace attached.",
25
+ });
22
26
 
23
27
  const turn = yield* triage("Close what is done.");
24
28
 
@@ -40,8 +44,12 @@ program.pipe(
40
44
  // No remote caller: this host names the member the assistant acts for, around the turn.
41
45
  // DEMO ONLY: a real host names whoever asked.
42
46
  Effect.provideService(CurrentMember, members.ada),
43
- Effect.provide(layer.pipe(Layer.provideMerge(Layer.merge(IssueStore.layerMemory, limiter)))),
44
- // DEMO ONLY: a scripted model. A real one is a provider's layer, in its place.
45
- Effect.provide(demoModel),
46
- NodeRuntime.runMain,
47
+ Effect.provide(
48
+ layer.pipe(
49
+ Layer.provideMerge(Layer.merge(IssueStore.layerMemory, limiter)),
50
+ // DEMO ONLY: a scripted model. A real one is a provider's layer, in its place.
51
+ Layer.provideMerge(demoModel)
52
+ )
53
+ ),
54
+ NodeRuntime.runMain()
47
55
  );
@@ -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 { CurrentMember } from "../identity/Identity.js";
4
5
 
5
6
  // The assistant behind a route: a model working on the issues with the tools of
@@ -16,31 +17,35 @@ import { CurrentMember } from "../identity/Identity.js";
16
17
  export const Turn = Schema.Struct({
17
18
  conversation: Schema.String,
18
19
  text: Schema.String,
19
- ran: Schema.Array(Schema.Struct({ tool: Schema.String, failed: Schema.Boolean })),
20
+ ran: Schema.Array(
21
+ Schema.Struct({ tool: Schema.String, failed: Schema.Boolean })
22
+ ),
20
23
  waiting: Schema.Array(
21
24
  Schema.Struct({
22
25
  approvalId: Schema.String,
23
26
  tool: Schema.String,
24
27
  // The arguments the model gave the call, as JSON text: what the member approves.
25
28
  arguments: Schema.String,
26
- }),
29
+ })
27
30
  ),
28
31
  });
29
32
 
30
33
  // Another member's conversation is not found either: an id tells nothing of who holds it.
31
- export class ConversationNotFound extends Schema.TaggedError<ConversationNotFound>()(
34
+ export class ConversationNotFound extends Schema.TaggedError<
35
+ ConversationNotFound,
36
+ { readonly brand: unique symbol }
37
+ >()(
32
38
  "ConversationNotFound",
33
39
  { conversation: Schema.String },
34
- { httpApiStatus: 404 },
40
+ { httpApiStatus: 404 }
35
41
  ) {}
36
42
 
37
43
  // The model failed in the middle of a turn. The conversation is over: its history holds
38
44
  // nothing of that turn, so it no longer says which calls ran.
39
- export class AssistantFailed extends Schema.TaggedError<AssistantFailed>()(
40
- "AssistantFailed",
41
- { message: Schema.String },
42
- { httpApiStatus: 502 },
43
- ) {}
45
+ export class AssistantFailed extends Schema.TaggedError<
46
+ AssistantFailed,
47
+ { readonly brand: unique symbol }
48
+ >()("AssistantFailed", { message: Schema.String }, { httpApiStatus: 502 }) {}
44
49
 
45
50
  export const AskAssistant = Action.make("askAssistant", {
46
51
  description:
@@ -58,19 +63,20 @@ export const AskAssistant = Action.make("askAssistant", {
58
63
  });
59
64
 
60
65
  export const AnswerAssistant = Action.make("answerAssistant", {
61
- description: "Approve or deny the calls a turn of your conversation waits for.",
66
+ description:
67
+ "Approve or deny the calls a turn of your conversation waits for.",
62
68
  input: {
63
69
  conversation: Schema.String,
64
70
  // Each request once. Effect acts on every answer it is given: an `approvalId` approved
65
71
  // twice in one request would run its call twice.
66
72
  answers: Schema.Array(
67
- Schema.Struct({ approvalId: Schema.String, approved: Schema.Boolean }),
73
+ Schema.Struct({ approvalId: Schema.String, approved: Schema.Boolean })
68
74
  ).check(
69
75
  Schema.makeFilter(
70
76
  (answers) =>
71
- new Set(answers.map(({ approvalId }) => approvalId)).size === answers.length ||
72
- "Answer each approvalId once",
73
- ),
77
+ new Set(answers.map(({ approvalId }) => approvalId)).size ===
78
+ answers.length || "Answer each approvalId once"
79
+ )
74
80
  ),
75
81
  },
76
82
  success: Turn,
@@ -1,5 +1,6 @@
1
1
  import { Context, Effect, Layer, Semaphore } from "effect";
2
2
  import { Chat } from "effect/ai";
3
+
3
4
  import type { Member } from "../identity/Identity.js";
4
5
  import { ConversationNotFound } from "./Assistant.js";
5
6
 
@@ -9,6 +10,11 @@ interface Conversation {
9
10
  readonly lock: Semaphore.Semaphore;
10
11
  }
11
12
 
13
+ // A member is a person in a workspace, and an id is unguessable: both are in the key, as
14
+ // JSON, which keeps its three parts apart whatever they hold.
15
+ const keyOf = (owner: Member, id: string) =>
16
+ JSON.stringify([owner.workspace, owner.id, id]);
17
+
12
18
  /**
13
19
  * Where conversations are kept between requests, each with the member it belongs to. A turn
14
20
  * names the member, so only its owner reaches a conversation: an approval is acted on as the
@@ -28,7 +34,7 @@ export class Conversations extends Context.Service<
28
34
  readonly turn: <A, E, R>(
29
35
  owner: Member,
30
36
  id: string,
31
- run: (chat: Chat.Chat) => Effect.Effect<A, E, R>,
37
+ run: (chat: Chat.Chat) => Effect.Effect<A, E, R>
32
38
  ) => Effect.Effect<A, E | ConversationNotFound, R>;
33
39
  }
34
40
  >()("tracker/Conversations") {
@@ -37,21 +43,20 @@ export class Conversations extends Context.Service<
37
43
  static readonly layerMemory = Layer.sync(Conversations, () => {
38
44
  const kept = new Map<string, Conversation>();
39
45
 
40
- // A member is a person in a workspace, and an id is unguessable: both are in the key, as
41
- // JSON, which keeps its three parts apart whatever they hold.
42
- const keyOf = (owner: Member, id: string) => JSON.stringify([owner.workspace, owner.id, id]);
43
-
44
46
  // Read when it runs, not when it is written: the second lookup of a turn runs under the lock.
45
47
  const find = (key: string, id: string) =>
46
48
  Effect.suspend(() => Effect.fromNullishOr(kept.get(key))).pipe(
47
- Effect.mapError(() => new ConversationNotFound({ conversation: id })),
49
+ Effect.mapError(() => new ConversationNotFound({ conversation: id }))
48
50
  );
49
51
 
50
52
  return Conversations.of({
51
53
  open: Effect.fn("Conversations.open")(function* (owner) {
52
54
  const id = crypto.randomUUID();
53
55
 
54
- kept.set(keyOf(owner, id), { chat: yield* Chat.empty, lock: yield* Semaphore.make(1) });
56
+ kept.set(keyOf(owner, id), {
57
+ chat: yield* Chat.empty,
58
+ lock: yield* Semaphore.make(1),
59
+ });
55
60
 
56
61
  return id;
57
62
  }),
@@ -62,8 +67,8 @@ export class Conversations extends Context.Service<
62
67
  // Looked up again once the lock is held: a turn that waited for one that failed
63
68
  // must find its conversation gone, not run on the history that turn left behind.
64
69
  Effect.flatMap(find(key, id), ({ chat }) =>
65
- Effect.onError(run(chat), () => Effect.sync(() => kept.delete(key))),
66
- ).pipe(lock.withPermits(1)),
70
+ Effect.onError(run(chat), () => Effect.sync(() => kept.delete(key)))
71
+ ).pipe(lock.withPermits(1))
67
72
  );
68
73
  },
69
74
  });
@@ -26,8 +26,8 @@ export const demoModel = Layer.effect(
26
26
  name: "closeIssue",
27
27
  params: { id: "ACME-1" },
28
28
  } as const,
29
- ],
29
+ ]
30
30
  ),
31
31
  streamText: () => Stream.empty,
32
- }),
32
+ })
33
33
  );
@@ -1,7 +1,9 @@
1
- import { Effect, Ref, Schema } from "effect";
2
- import { type Chat, LanguageModel, type Prompt } from "effect/ai";
3
1
  import * as Action from "@gjermundgaraba/effect-actions/Action";
4
2
  import * as ActionToolkit from "@gjermundgaraba/effect-actions/ActionToolkit";
3
+ import { Effect, Ref, Schema } from "effect";
4
+ import { LanguageModel } from "effect/ai";
5
+ import type { Chat, Prompt } from "effect/ai";
6
+
5
7
  import { CurrentMember } from "../identity/Identity.js";
6
8
  import { toolkit } from "../issues/assistant.js";
7
9
  import { AssistantFailed, Assistant } from "./Assistant.js";
@@ -10,7 +12,10 @@ import { Conversations } from "./conversations.js";
10
12
  // A model that keeps calling tools is asked this many times in one turn, and no more.
11
13
  const requestsPerTurn = 6;
12
14
 
13
- type Response = LanguageModel.GenerateTextResponse<typeof toolkit.tools, "opaque">;
15
+ type Response = LanguageModel.GenerateTextResponse<
16
+ typeof toolkit.tools,
17
+ "opaque"
18
+ >;
14
19
 
15
20
  const approvalsOf = (response: Response) =>
16
21
  response.content.filter((part) => part.type === "tool-approval-request");
@@ -19,7 +24,9 @@ const approvalsOf = (response: Response) =>
19
24
  // or one in a reply the provider cut short. Left out of `ran` below, which lists the calls that
20
25
  // ran.
21
26
  const didNotRun = Schema.is(
22
- Schema.Struct({ type: Schema.Literals(["execution-denied", "execution-interrupted"]) }),
27
+ Schema.Struct({
28
+ type: Schema.Literals(["execution-denied", "execution-interrupted"]),
29
+ })
23
30
  );
24
31
 
25
32
  const callsToRead = (response: Response) =>
@@ -38,7 +45,11 @@ export const assistant = Action.implement(
38
45
  const model = yield* LanguageModel.LanguageModel;
39
46
  const conversations = yield* Conversations;
40
47
 
41
- const converse = (conversation: string, chat: Chat.Chat, prompt: Prompt.RawInput) =>
48
+ const converse = (
49
+ conversation: string,
50
+ chat: Chat.Chat,
51
+ prompt: Prompt.RawInput
52
+ ) =>
42
53
  Effect.gen(function* () {
43
54
  // One `generateText` is one request to the model: its tool calls run, and their
44
55
  // results come back unread. The model is asked again, with nothing new, while it
@@ -48,7 +59,11 @@ export const assistant = Action.implement(
48
59
  let response = yield* chat.generateText({ prompt, toolkit: tools });
49
60
  const results = [...response.toolResults];
50
61
 
51
- for (let asked = 1; asked < requestsPerTurn && callsToRead(response); asked++) {
62
+ for (
63
+ let asked = 1;
64
+ asked < requestsPerTurn && callsToRead(response);
65
+ asked += 1
66
+ ) {
52
67
  response = yield* chat.generateText({ prompt: [], toolkit: tools });
53
68
  results.push(...response.toolResults);
54
69
  }
@@ -57,45 +72,50 @@ export const assistant = Action.implement(
57
72
  conversation,
58
73
  text: response.text,
59
74
  ran: results
75
+ .values()
60
76
  .filter(({ result }) => !didNotRun(result))
61
- .map(({ name, isFailure }) => ({ tool: name, failed: isFailure })),
77
+ .map(({ name, isFailure }) => ({ tool: name, failed: isFailure }))
78
+ .toArray(),
62
79
  // Every request of the conversation still waiting for an answer, with the call it
63
80
  // would run, read from the `Chat`'s history by the rule Effect applies when an answer
64
81
  // arrives: one an earlier answer left open is listed in every turn until answered.
65
82
  // The arguments are shown as JSON, as the member approves them.
66
- waiting: ActionToolkit.openApprovals(yield* Ref.get(chat.history)).map(
67
- ({ approvalId, name, params }) => ({
68
- approvalId,
69
- tool: name,
70
- arguments: JSON.stringify(params),
71
- }),
72
- ),
83
+ waiting: ActionToolkit.openApprovals(
84
+ yield* Ref.get(chat.history)
85
+ ).map(({ approvalId, name, params }) => ({
86
+ approvalId,
87
+ tool: name,
88
+ arguments: JSON.stringify(params),
89
+ })),
73
90
  };
74
91
  }).pipe(
75
92
  // The model is a startup service, given to each turn here.
76
- Effect.provideService(LanguageModel.LanguageModel, model),
93
+ Effect.provideService(LanguageModel.LanguageModel, model)
77
94
  );
78
95
 
79
96
  // A failed turn says what may have run.
80
97
  const turn = (conversation: string, prompt: Prompt.RawInput) =>
81
98
  Effect.flatMap(CurrentMember, (member) =>
82
- conversations.turn(member, conversation, (chat) => converse(conversation, chat, prompt)),
99
+ conversations.turn(member, conversation, (chat) =>
100
+ converse(conversation, chat, prompt)
101
+ )
83
102
  ).pipe(
84
103
  Effect.catchTag("AiError", () =>
85
104
  Effect.fail(
86
105
  new AssistantFailed({
87
106
  message:
88
107
  "The model failed and this conversation has ended. A call you approved in this turn may have run: read the issue before asking again.",
89
- }),
90
- ),
91
- ),
108
+ })
109
+ )
110
+ )
92
111
  );
93
112
 
94
113
  return {
95
114
  askAssistant: ({ prompt, conversation }) =>
96
115
  conversation === undefined
97
- ? Effect.flatMap(Effect.flatMap(CurrentMember, conversations.open), (opened) =>
98
- turn(opened, prompt),
116
+ ? Effect.flatMap(
117
+ Effect.flatMap(CurrentMember, conversations.open),
118
+ (opened) => turn(opened, prompt)
99
119
  )
100
120
  : turn(conversation, prompt),
101
121
  // The answers start the next turn, in which an approved call runs, as this request's
@@ -117,5 +137,5 @@ export const assistant = Action.implement(
117
137
  }),
118
138
  // Any member may ask. What the assistant may do for them is each tool's rule, not this one:
119
139
  // a member without the write scope gets the tool's refusal as a result the model reads.
120
- { authorize: Action.allowAll },
140
+ { authorize: Action.allowAll }
121
141
  );
@@ -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 { CurrentMember } from "../identity/Identity.js";
4
5
  import { IssueWriteErrors } from "../issues/Issues.js";
5
6
 
@@ -13,11 +14,10 @@ export const Attachment = Schema.Struct({
13
14
 
14
15
  export type Attachment = typeof Attachment.Type;
15
16
 
16
- export class AttachmentNotFound extends Schema.TaggedError<AttachmentNotFound>()(
17
- "AttachmentNotFound",
18
- { id: Schema.String },
19
- { httpApiStatus: 404 },
20
- ) {}
17
+ export class AttachmentNotFound extends Schema.TaggedError<
18
+ AttachmentNotFound,
19
+ { readonly brand: unique symbol }
20
+ >()("AttachmentNotFound", { id: Schema.String }, { httpApiStatus: 404 }) {}
21
21
 
22
22
  // `Action.Image` is `{ data: Uint8Array, mimeType: string }`. In an input it is JSON over HTTP,
23
23
  // RPC and the CLI, the bytes in base64.
@@ -1,6 +1,7 @@
1
+ import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
1
2
  import { Effect, Layer } from "effect";
2
3
  import { HttpRouter, HttpServerResponse } from "effect/http";
3
- import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
4
+
4
5
  import { authorize } from "../identity/authorize.js";
5
6
  import { CurrentMember, Session } from "../identity/Identity.js";
6
7
  import { GetAttachment } from "./Attachments.js";
@@ -38,9 +39,9 @@ export const download = Layer.unwrap(
38
39
  });
39
40
  }).pipe(
40
41
  Effect.catchTag("AttachmentNotFound", () =>
41
- Effect.succeed(HttpServerResponse.empty({ status: 404 })),
42
- ),
43
- ),
44
- ).pipe(Layer.provide(Authentication.protect(Session).layer)),
45
- ),
42
+ Effect.succeed(HttpServerResponse.empty({ status: 404 }))
43
+ )
44
+ )
45
+ ).pipe(Layer.provide(Authentication.protect(Session).layer))
46
+ )
46
47
  );
@@ -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 { CurrentMember } from "../identity/Identity.js";
5
6
  import { writeLimit } from "../issues/write-limit.js";
@@ -27,11 +28,15 @@ export const attachments = Action.implement(
27
28
  });
28
29
  }
29
30
 
30
- return yield* limited((member) => store.add(member.workspace, issue, name, image));
31
+ return yield* limited((member) =>
32
+ store.add(member.workspace, issue, name, image)
33
+ );
31
34
  }),
32
35
  getAttachment: ({ id }) =>
33
- Effect.flatMap(CurrentMember, (member) => store.get(member.workspace, id)),
36
+ Effect.flatMap(CurrentMember, (member) =>
37
+ store.get(member.workspace, id)
38
+ ),
34
39
  };
35
40
  }),
36
- { authorize },
41
+ { authorize }
37
42
  );
@@ -1,8 +1,10 @@
1
- import { Context, Effect, Layer } from "effect";
2
1
  import type * as Action from "@gjermundgaraba/effect-actions/Action";
2
+ import { Context, Effect, Layer } from "effect";
3
+
3
4
  import type { IssueNotFound } from "../issues/Issues.js";
4
5
  import { IssueStore } from "../issues/store.js";
5
- import { type Attachment, AttachmentNotFound } from "./Attachments.js";
6
+ import { AttachmentNotFound } from "./Attachments.js";
7
+ import type { Attachment } from "./Attachments.js";
6
8
 
7
9
  type Image = typeof Action.Image.Type;
8
10
 
@@ -19,9 +21,12 @@ export class AttachmentStore extends Context.Service<
19
21
  workspace: string,
20
22
  issue: string,
21
23
  name: string,
22
- image: Image,
24
+ image: Image
23
25
  ) => Effect.Effect<Attachment, IssueNotFound>;
24
- readonly get: (workspace: string, id: string) => Effect.Effect<Stored, AttachmentNotFound>;
26
+ readonly get: (
27
+ workspace: string,
28
+ id: string
29
+ ) => Effect.Effect<Stored, AttachmentNotFound>;
25
30
  }
26
31
  >()("tracker/AttachmentStore") {
27
32
  // DEMO ONLY: memory. A service captures what it depends on when it is built, here the issue
@@ -33,26 +38,28 @@ export class AttachmentStore extends Context.Service<
33
38
  const stored = new Map<string, Stored>();
34
39
 
35
40
  return AttachmentStore.of({
36
- add: Effect.fn("AttachmentStore.add")(function* (workspace, issue, name, image) {
37
- yield* issues.get(workspace, issue);
41
+ add: Effect.fn("AttachmentStore.add")(
42
+ function* (workspace, issue, name, image) {
43
+ yield* issues.get(workspace, issue);
38
44
 
39
- const attachment = {
40
- id: crypto.randomUUID(),
41
- issue,
42
- name,
43
- mimeType: image.mimeType,
44
- size: image.data.byteLength,
45
- };
45
+ const attachment = {
46
+ id: crypto.randomUUID(),
47
+ issue,
48
+ name,
49
+ mimeType: image.mimeType,
50
+ size: image.data.byteLength,
51
+ };
46
52
 
47
- stored.set(`${workspace}/${attachment.id}`, { attachment, image });
53
+ stored.set(`${workspace}/${attachment.id}`, { attachment, image });
48
54
 
49
- return attachment;
50
- }),
55
+ return attachment;
56
+ }
57
+ ),
51
58
  get: (workspace, id) =>
52
59
  Effect.fromNullishOr(stored.get(`${workspace}/${id}`)).pipe(
53
- Effect.mapError(() => new AttachmentNotFound({ id })),
60
+ Effect.mapError(() => new AttachmentNotFound({ id }))
54
61
  ),
55
62
  });
56
- }),
63
+ })
57
64
  );
58
65
  }
@@ -1,7 +1,8 @@
1
+ import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
1
2
  import { Effect, Layer } from "effect";
2
3
  import { RpcClient, RpcSerialization } from "effect/rpc";
3
4
  import { Socket } from "effect/socket";
4
- import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
5
+
5
6
  import { Rpc } from "./Rpc.js";
6
7
 
7
8
  // Entrypoint of the web app, an Effect program in a page. It imports the RPC binding and
@@ -22,11 +23,16 @@ export const board = Effect.gen(function* () {
22
23
  export const protocol = (url: string) =>
23
24
  RpcClient.layerProtocolSocket().pipe(
24
25
  Layer.provide(Socket.layerWebSocket(url)),
25
- Layer.provide([RpcSerialization.layerJson, Socket.layerWebSocketConstructorGlobal]),
26
+ Layer.provide([
27
+ RpcSerialization.layerJson,
28
+ Socket.layerWebSocketConstructorGlobal,
29
+ ])
26
30
  );
27
31
 
28
32
  // `open("wss://tracker.example.com/rpc")`: one run on a socket of its own, closed when it ends.
29
33
  // A page that stays connected builds the protocol's Layer once, such as in a `ManagedRuntime`,
30
34
  // and runs its programs in it.
31
- export const open = (url: string) =>
32
- Effect.runPromise(board.pipe(Effect.scoped, Effect.provide(protocol(url))));
35
+ export const open = async (url: string) =>
36
+ await Effect.runPromise(
37
+ board.pipe(Effect.scoped, Effect.provide(protocol(url)))
38
+ );