@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,5 +1,7 @@
1
1
  import { NodeServices } from "@effect/platform-node";
2
2
  import { describe, expect, it } from "@effect/vitest";
3
+ import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
4
+ import * as Testing from "@gjermundgaraba/effect-actions/Testing";
3
5
  import {
4
6
  Arbitrary,
5
7
  ConfigProvider,
@@ -13,8 +15,7 @@ import {
13
15
  import { CliError, Command } from "effect/cli";
14
16
  import { HttpClient, HttpClientRequest } from "effect/http";
15
17
  import { TestConsole } from "effect/testing";
16
- import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
17
- import * as Testing from "@gjermundgaraba/effect-actions/Testing";
18
+
18
19
  import { Http } from "../src/Api.js";
19
20
  import { commands } from "../src/commands.js";
20
21
  import { ImportNotes, Note, NoteNotFound } from "../src/notes/Notes.js";
@@ -30,21 +31,30 @@ const served = Testing.layer(routes).pipe(Layer.provide(NoteStore.layerMemory));
30
31
  // the environment as a Layer, so the test decides whether a token is configured, whatever the
31
32
  // shell holds. The console of `it.effect` keeps every line printed, so a run returns the lines
32
33
  // it added.
33
- const connected = (environment: Record<string, string>, args: ReadonlyArray<string>, stdin = "") =>
34
+ const connected = (
35
+ environment: Record<string, string>,
36
+ args: readonly string[],
37
+ stdin = ""
38
+ ) =>
34
39
  Effect.gen(function* () {
35
40
  const before = (yield* TestConsole.logLines).length;
36
41
 
37
42
  yield* Command.runWith(commands, { version: "0" })(args).pipe(
38
- Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnvRecord(environment))),
39
- Effect.provide(Stdio.layerTest({ stdin: Stream.make(new TextEncoder().encode(stdin)) })),
40
- Effect.provide(NodeServices.layer),
43
+ Effect.provide(
44
+ Layer.merge(
45
+ ConfigProvider.layer(ConfigProvider.fromEnvRecord(environment)),
46
+ Stdio.layerTest({
47
+ stdin: Stream.make(new TextEncoder().encode(stdin)),
48
+ })
49
+ ).pipe(Layer.provideMerge(NodeServices.layer))
50
+ )
41
51
  );
42
52
 
43
53
  return (yield* TestConsole.logLines).slice(before);
44
54
  });
45
55
 
46
56
  // A run as the caller `token` names.
47
- const run = (token: string, args: ReadonlyArray<string>, stdin = "") =>
57
+ const run = (token: string, args: readonly string[], stdin = "") =>
48
58
  connected({ NOTES_TOKEN: token }, args, stdin);
49
59
 
50
60
  const fitsAFile = (text: string) =>
@@ -61,8 +71,8 @@ const trickyLines = Arbitrary.array(
61
71
  "#tag",
62
72
  "\r# After a carriage return",
63
73
  "\u2028# After a line separator",
64
- ]),
65
- ),
74
+ ])
75
+ )
66
76
  );
67
77
 
68
78
  const importable = Arbitrary.all([
@@ -73,35 +83,62 @@ const importable = Arbitrary.all([
73
83
  notes.map(({ title, body }, place) => ({
74
84
  title,
75
85
  body: [body, ...(extra[place] ?? [])].join("\n"),
76
- })),
86
+ }))
77
87
  ),
78
88
  Arbitrary.filter((notes) =>
79
- notes.every(({ title, body }) => fitsAFile(title) && fitsAFile(body)),
80
- ),
89
+ notes.every(({ title, body }) => fitsAFile(title) && fitsAFile(body))
90
+ )
81
91
  );
82
92
 
93
+ const as = (token: string) =>
94
+ ActionHttp.client(Http).pipe(
95
+ Effect.updateService(
96
+ HttpClient.HttpClient,
97
+ HttpClient.mapRequest(HttpClientRequest.bearerToken(token))
98
+ )
99
+ );
100
+
83
101
  describe("the commands", () => {
84
- it.effect("take an argument, a flag and stdin, and print what `render` returns", () =>
85
- Effect.gen(function* () {
86
- // A command without `render` prints its result as JSON.
87
- const [created] = yield* run("ada", ["create-note", "--title", "Plan"], "First draft\n");
88
- const { id } = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(Note))(created);
102
+ it.effect(
103
+ "take an argument, a flag and stdin, and print what `render` returns",
104
+ () =>
105
+ Effect.gen(function* () {
106
+ // A command without `render` prints its result as JSON.
107
+ const [created] = yield* run(
108
+ "ada",
109
+ ["create-note", "--title", "Plan"],
110
+ "First draft\n"
111
+ );
89
112
 
90
- expect(yield* run("ada", ["get-note", id])).toEqual(["# Plan\n\nFirst draft"]);
113
+ const { id } = yield* Schema.decodeUnknownEffect(
114
+ Schema.fromJsonString(Note)
115
+ )(created);
91
116
 
92
- yield* run("ada", ["update-note", id, "--revision", "1"], "Second draft\n");
117
+ expect(yield* run("ada", ["get-note", id])).toStrictEqual([
118
+ "# Plan\n\nFirst draft",
119
+ ]);
93
120
 
94
- expect(yield* run("ada", ["get-note", id])).toEqual(["# Plan\n\nSecond draft"]);
95
- }).pipe(Effect.provide(served)),
121
+ yield* run(
122
+ "ada",
123
+ ["update-note", id, "--revision", "1"],
124
+ "Second draft\n"
125
+ );
126
+
127
+ expect(yield* run("ada", ["get-note", id])).toStrictEqual([
128
+ "# Plan\n\nSecond draft",
129
+ ]);
130
+ }).pipe(Effect.provide(served))
96
131
  );
97
132
 
98
- it.effect("describe a flag in `--help` as the contract describes its field", () =>
99
- Effect.gen(function* () {
100
- // Help is one printed text. The description is the `revision` field's, from Notes.ts.
101
- expect(yield* run("ada", ["update-note", "--help"])).toEqual([
102
- expect.stringContaining("The revision you read: 1 for a new note"),
103
- ]);
104
- }).pipe(Effect.provide(served)),
133
+ it.effect(
134
+ "describe a flag in `--help` as the contract describes its field",
135
+ () =>
136
+ Effect.gen(function* () {
137
+ // Help is one printed text. The description is the `revision` field's, from Notes.ts.
138
+ expect(yield* run("ada", ["update-note", "--help"])).toStrictEqual([
139
+ expect.stringContaining("The revision you read: 1 for a new note"),
140
+ ]);
141
+ }).pipe(Effect.provide(served))
105
142
  );
106
143
 
107
144
  it.effect("run a command of their own beside the derived ones", () =>
@@ -113,11 +150,13 @@ describe("the commands", () => {
113
150
  const files = yield* FileSystem.FileSystem;
114
151
  const file = `${yield* files.makeTempDirectoryScoped()}/notes.md`;
115
152
 
116
- expect(yield* run("ada", ["export", file])).toEqual([`Wrote 2 notes to ${file}`]);
153
+ expect(yield* run("ada", ["export", file])).toStrictEqual([
154
+ `Wrote 2 notes to ${file}`,
155
+ ]);
117
156
  expect(yield* files.readFileString(file)).toBe(
118
- "# Plan\n\nFirst draft\n\n# Budget\n\nToo small",
157
+ "# Plan\n\nFirst draft\n\n# Budget\n\nToo small"
119
158
  );
120
- }).pipe(Effect.provide(served), Effect.provide(NodeServices.layer)),
159
+ }).pipe(Effect.provide(Layer.merge(served, NodeServices.layer)))
121
160
  );
122
161
 
123
162
  it.effect("import a file `export` wrote, all its notes or none", () =>
@@ -126,38 +165,52 @@ describe("the commands", () => {
126
165
  const directory = yield* files.makeTempDirectoryScoped();
127
166
  const file = `${directory}/notes.md`;
128
167
 
129
- yield* files.writeFileString(file, "# Plan\n\nFirst draft\n\n# Budget\n\nToo small");
168
+ yield* files.writeFileString(
169
+ file,
170
+ "# Plan\n\nFirst draft\n\n# Budget\n\nToo small"
171
+ );
130
172
 
131
- expect(yield* run("ada", ["import", file])).toEqual(["Created 2 notes"]);
132
- expect(yield* run("grace", ["import", file])).toEqual(["Created 2 notes"]);
173
+ expect(yield* run("ada", ["import", file])).toStrictEqual([
174
+ "Created 2 notes",
175
+ ]);
176
+ expect(yield* run("grace", ["import", file])).toStrictEqual([
177
+ "Created 2 notes",
178
+ ]);
133
179
 
134
- yield* files.writeFileString(`${directory}/untitled.md`, "# Plan\n\nKept?\n\n# \n\nNo title");
180
+ yield* files.writeFileString(
181
+ `${directory}/untitled.md`,
182
+ "# Plan\n\nKept?\n\n# \n\nNo title"
183
+ );
135
184
 
136
- const refused = yield* Effect.flip(run("ada", ["import", `${directory}/untitled.md`]));
185
+ const refused = yield* Effect.flip(
186
+ run("ada", ["import", `${directory}/untitled.md`])
187
+ );
137
188
 
138
189
  expect(refused).toHaveProperty(
139
190
  "userMessage",
140
- expect.stringContaining("A note needs a title."),
191
+ expect.stringContaining("A note needs a title.")
141
192
  );
142
193
  const [listed = ""] = yield* run("ada", ["list-notes"]);
143
194
 
144
195
  const { notes } = yield* Schema.decodeUnknownEffect(
145
196
  Schema.fromJsonString(
146
- Schema.Struct({ notes: Schema.Array(Schema.Struct({ title: Schema.String })) }),
147
- ),
197
+ Schema.Struct({
198
+ notes: Schema.Array(Schema.Struct({ title: Schema.String })),
199
+ })
200
+ )
148
201
  )(listed);
149
202
 
150
- expect(notes.map(({ title }) => title)).toEqual(["Plan", "Budget"]);
203
+ expect(notes.map(({ title }) => title)).toStrictEqual(["Plan", "Budget"]);
151
204
 
152
205
  // A missing file is a usage error: the command's help, and the file named on stderr.
153
206
  yield* Effect.flip(run("ada", ["import", `${directory}/nowhere.md`]));
154
207
 
155
208
  expect(yield* TestConsole.errorLines).toContainEqual(
156
209
  expect.stringMatching(
157
- /Invalid value for argument <file>: .*nowhere\.md.*File does not exist/su,
158
- ),
210
+ /Invalid value for argument <file>: .*nowhere\.md.*File does not exist/su
211
+ )
159
212
  );
160
- }).pipe(Effect.provide(served), Effect.provide(NodeServices.layer)),
213
+ }).pipe(Effect.provide(Layer.merge(served, NodeServices.layer)))
161
214
  );
162
215
 
163
216
  // Any notes the contract accepts, generated from its schema, with lines that start as a title
@@ -171,14 +224,6 @@ describe("the commands", () => {
171
224
  const files = yield* FileSystem.FileSystem;
172
225
  const file = `${yield* files.makeTempDirectoryScoped()}/notes.md`;
173
226
 
174
- const as = (token: string) =>
175
- ActionHttp.client(Http).pipe(
176
- Effect.updateService(
177
- HttpClient.HttpClient,
178
- HttpClient.mapRequest(HttpClientRequest.bearerToken(token)),
179
- ),
180
- );
181
-
182
227
  const [ada, grace] = [yield* as("ada"), yield* as("grace")];
183
228
 
184
229
  yield* ada.importNotes({ notes });
@@ -186,11 +231,16 @@ describe("the commands", () => {
186
231
  yield* run("grace", ["import", file]);
187
232
 
188
233
  const { notes: listed } = yield* grace.listNotes();
189
- const read = yield* Effect.forEach(listed, ({ id }) => grace.getNote({ id }));
190
234
 
191
- expect(read.map(({ title, body }) => ({ title, body }))).toEqual(notes);
192
- }).pipe(Effect.provide(served), Effect.provide(NodeServices.layer)),
193
- { arbitrary: { runs: 50 } },
235
+ const read = yield* Effect.forEach(listed, ({ id }) =>
236
+ grace.getNote({ id })
237
+ );
238
+
239
+ expect(read.map(({ title, body }) => ({ title, body }))).toStrictEqual(
240
+ notes
241
+ );
242
+ }).pipe(Effect.provide(Layer.merge(served, NodeServices.layer))),
243
+ { arbitrary: { runs: 50 } }
194
244
  );
195
245
 
196
246
  it.effect(
@@ -202,82 +252,105 @@ describe("the commands", () => {
202
252
 
203
253
  yield* files.writeFileString(`${directory}/empty.md`, "");
204
254
 
205
- expect(yield* run("grace", ["import", `${directory}/empty.md`])).toEqual([
206
- "Created 0 notes",
207
- ]);
255
+ expect(
256
+ yield* run("grace", ["import", `${directory}/empty.md`])
257
+ ).toStrictEqual(["Created 0 notes"]);
208
258
 
209
- yield* files.writeFileString(`${directory}/preamble.md`, "My notes\n\n# Plan\n\nDraft");
259
+ yield* files.writeFileString(
260
+ `${directory}/preamble.md`,
261
+ "My notes\n\n# Plan\n\nDraft"
262
+ );
210
263
 
211
264
  expect(
212
- yield* Effect.flip(run("grace", ["import", `${directory}/preamble.md`])),
265
+ yield* Effect.flip(
266
+ run("grace", ["import", `${directory}/preamble.md`])
267
+ )
213
268
  ).toHaveProperty(
214
269
  "userMessage",
215
- "The file does not start with a `# ` title, as `notes export` writes one.",
270
+ "The file does not start with a `# ` title, as `notes export` writes one."
216
271
  );
217
272
 
218
- const many = Array.from({ length: 101 }, (_, place) => `# Note ${place}\n\nBody`);
273
+ const many = Array.from(
274
+ { length: 101 },
275
+ (_, place) => `# Note ${place}\n\nBody`
276
+ );
219
277
 
220
278
  yield* files.writeFileString(`${directory}/many.md`, many.join("\n\n"));
221
279
 
222
- expect(yield* Effect.flip(run("grace", ["import", `${directory}/many.md`]))).toHaveProperty(
280
+ expect(
281
+ yield* Effect.flip(run("grace", ["import", `${directory}/many.md`]))
282
+ ).toHaveProperty(
223
283
  "userMessage",
224
- expect.stringContaining("An import holds at most 100 notes."),
284
+ expect.stringContaining("An import holds at most 100 notes.")
225
285
  );
226
286
 
227
- expect(yield* run("grace", ["list-notes"])).toEqual([
287
+ expect(yield* run("grace", ["list-notes"])).toStrictEqual([
228
288
  JSON.stringify({ notes: [] }, null, 2),
229
289
  ]);
230
- }).pipe(Effect.provide(served), Effect.provide(NodeServices.layer)),
290
+ }).pipe(Effect.provide(Layer.merge(served, NodeServices.layer)))
231
291
  );
232
292
 
233
293
  it.effect("fail as the server refuses, a command of their own included", () =>
234
294
  Effect.gen(function* () {
235
- const refused = yield* Effect.flip(run("mallory", ["export", "notes.md"]));
295
+ const refused = yield* Effect.flip(
296
+ run("mallory", ["export", "notes.md"])
297
+ );
236
298
 
237
299
  expect(refused).toBeInstanceOf(CliError.UserError);
238
300
  expect(refused).toHaveProperty(
239
301
  "userMessage",
240
- '{"_tag":"Unauthenticated","message":"Unknown demo token."}',
302
+ '{"_tag":"Unauthenticated","message":"Unknown demo token."}'
241
303
  );
242
304
  // The command's own context is a line of its own on stderr, the JSON left as it is.
243
- expect(yield* TestConsole.errorLines).toContain("The export to notes.md did not finish.");
305
+ expect(yield* TestConsole.errorLines).toContain(
306
+ "The export to notes.md did not finish."
307
+ );
244
308
 
245
309
  // Not an action's error: the file system's, named in the message.
246
- const unwritable = yield* Effect.flip(run("ada", ["export", "no-such-directory/notes.md"]));
310
+ const unwritable = yield* Effect.flip(
311
+ run("ada", ["export", "no-such-directory/notes.md"])
312
+ );
247
313
 
248
314
  expect(unwritable).toBeInstanceOf(CliError.UserError);
249
- expect(unwritable).toHaveProperty("userMessage", expect.stringMatching(/^PlatformError: /));
315
+ expect(unwritable).toHaveProperty(
316
+ "userMessage",
317
+ expect.stringMatching(/^PlatformError: /u)
318
+ );
250
319
 
251
320
  const missing = yield* Effect.flip(run("ada", ["get-note", "9"]));
252
321
 
253
322
  expect(missing).toHaveProperty("cause", new NoteNotFound({ id: "9" }));
254
- }).pipe(Effect.provide(served)),
323
+ }).pipe(Effect.provide(served))
255
324
  );
256
325
 
257
- it.effect("call as the configured token names, and ask for one where none is configured", () =>
258
- Effect.gen(function* () {
259
- yield* connected({ NOTES_TOKEN: "reader" }, ["who-am-i"]);
326
+ it.effect(
327
+ "call as the configured token names, and ask for one where none is configured",
328
+ () =>
329
+ Effect.gen(function* () {
330
+ yield* connected({ NOTES_TOKEN: "reader" }, ["who-am-i"]);
260
331
 
261
- expect(yield* TestConsole.logLines).toEqual([expect.stringContaining('"id": "reader"')]);
332
+ expect(yield* TestConsole.logLines).toStrictEqual([
333
+ expect.stringContaining('"id": "reader"'),
334
+ ]);
262
335
 
263
- const refused = yield* Effect.flip(connected({}, ["list-notes"]));
336
+ const refused = yield* Effect.flip(connected({}, ["list-notes"]));
264
337
 
265
- expect(refused).toBeInstanceOf(CliError.UserError);
266
- expect(refused).toHaveProperty("cause._tag", "MissingToken");
267
- expect(refused).toHaveProperty(
268
- "userMessage",
269
- "MissingToken: NOTES_TOKEN is not set. Set it to your token: export NOTES_TOKEN=<your token>",
270
- );
338
+ expect(refused).toBeInstanceOf(CliError.UserError);
339
+ expect(refused).toHaveProperty("cause._tag", "MissingToken");
340
+ expect(refused).toHaveProperty(
341
+ "userMessage",
342
+ "MissingToken: NOTES_TOKEN is not set. Set it to your token: export NOTES_TOKEN=<your token>"
343
+ );
271
344
 
272
- // What a person sees: that message, once, on stderr, and no report of a `ConfigError`.
273
- expect(yield* TestConsole.errorLines).toEqual([
274
- expect.stringContaining("NOTES_TOKEN is not set."),
275
- ]);
345
+ // What a person sees: that message, once, on stderr, and no report of a `ConfigError`.
346
+ expect(yield* TestConsole.errorLines).toStrictEqual([
347
+ expect.stringContaining("NOTES_TOKEN is not set."),
348
+ ]);
276
349
 
277
- // The token is read when a command runs, so help needs none.
278
- yield* connected({}, ["list-notes", "--help"]);
350
+ // The token is read when a command runs, so help needs none.
351
+ yield* connected({}, ["list-notes", "--help"]);
279
352
 
280
- expect(yield* TestConsole.logLines).toHaveLength(2);
281
- }).pipe(Effect.provide(served)),
353
+ expect(yield* TestConsole.logLines).toHaveLength(2);
354
+ }).pipe(Effect.provide(served))
282
355
  );
283
356
  });
@@ -1,6 +1,8 @@
1
1
  import { Effect } from "effect";
2
+
2
3
  import { CurrentUser } from "../../src/identity/Identity.js";
3
4
  import { users } from "../../src/identity/users.js";
4
5
 
5
6
  // The caller of one call, as a server provides it per request.
6
- export const as = (user: keyof typeof users) => Effect.provideService(CurrentUser, users[user]);
7
+ export const as = (user: keyof typeof users) =>
8
+ Effect.provideService(CurrentUser, users[user]);
@@ -1,6 +1,7 @@
1
1
  import { expect, it } from "@effect/vitest";
2
- import { Arbitrary, Array, Effect, References, Schema } from "effect";
3
2
  import * as Action from "@gjermundgaraba/effect-actions/Action";
3
+ import { Arbitrary, Array, Effect, References, Schema } from "effect";
4
+
4
5
  import { notes } from "../../src/notes/handlers.js";
5
6
  import { CreateNote, RequestConflict } from "../../src/notes/Notes.js";
6
7
  import { NoteStore } from "../../src/notes/store.js";
@@ -14,13 +15,13 @@ it.effect("creates a new note on every call without a request key", () =>
14
15
  const second = yield* client.createNote(input);
15
16
 
16
17
  expect(second.id).not.toBe(first.id);
17
- expect(yield* client.listNotes()).toEqual({
18
+ expect(yield* client.listNotes()).toStrictEqual({
18
19
  notes: [
19
20
  { id: first.id, title: input.title },
20
21
  { id: second.id, title: input.title },
21
22
  ],
22
23
  });
23
- }).pipe(as("ada"), Effect.provide(NoteStore.layerMemory)),
24
+ }).pipe(as("ada"), Effect.provide(NoteStore.layerMemory))
24
25
  );
25
26
 
26
27
  it.effect.prop(
@@ -30,7 +31,7 @@ it.effect.prop(
30
31
  Schema.Struct({
31
32
  ...CreateNote.input.fields,
32
33
  requestId: Schema.requiredKey(CreateNote.input.fields.requestId),
33
- }),
34
+ })
34
35
  ),
35
36
  },
36
37
  ({ input }) =>
@@ -39,12 +40,16 @@ it.effect.prop(
39
40
  const first = yield* client.createNote(input);
40
41
  const replay = yield* client.createNote(input);
41
42
 
42
- expect(first).toMatchObject({ title: input.title, body: input.body, revision: 1 });
43
- expect(replay).toEqual(first);
44
- expect(yield* client.listNotes()).toEqual({
43
+ expect(first).toMatchObject({
44
+ title: input.title,
45
+ body: input.body,
46
+ revision: 1,
47
+ });
48
+ expect(replay).toStrictEqual(first);
49
+ expect(yield* client.listNotes()).toStrictEqual({
45
50
  notes: [{ id: first.id, title: input.title }],
46
51
  });
47
- }).pipe(as("ada"), Effect.provide(NoteStore.layerMemory)),
52
+ }).pipe(as("ada"), Effect.provide(NoteStore.layerMemory))
48
53
  );
49
54
 
50
55
  it.effect.each([
@@ -57,73 +62,98 @@ it.effect.each([
57
62
  const first = yield* client.createNote(input);
58
63
 
59
64
  expect(
60
- yield* Effect.flip(client.createNote({ requestId: input.requestId, ...changed })),
61
- ).toEqual(new RequestConflict({ requestId: input.requestId }));
62
- expect(yield* client.createNote(input)).toEqual(first);
63
- expect(yield* client.listNotes()).toEqual({ notes: [{ id: first.id, title: "Plan" }] });
64
- }).pipe(as("ada"), Effect.provide(NoteStore.layerMemory)),
65
+ yield* Effect.flip(
66
+ client.createNote({ requestId: input.requestId, ...changed })
67
+ )
68
+ ).toStrictEqual(new RequestConflict({ requestId: input.requestId }));
69
+ expect(yield* client.createNote(input)).toStrictEqual(first);
70
+ expect(yield* client.listNotes()).toStrictEqual({
71
+ notes: [{ id: first.id, title: "Plan" }],
72
+ });
73
+ }).pipe(as("ada"), Effect.provide(NoteStore.layerMemory))
65
74
  );
66
75
 
67
- it.effect("scopes keys to the caller, and treats a new key as a new creation", () =>
68
- Effect.gen(function* () {
69
- const client = yield* Action.client(notes);
70
- const input = { requestId: "plan", title: "Plan", body: "Draft" };
71
- const ada = yield* client.createNote(input).pipe(as("ada"));
72
- const grace = yield* client.createNote({ ...input, body: "Grace's draft" }).pipe(as("grace"));
73
- const another = yield* client.createNote({ ...input, requestId: "another" }).pipe(as("ada"));
74
-
75
- expect(new Set([ada.id, grace.id, another.id]).size).toBe(3);
76
- expect(yield* client.createNote(input).pipe(as("ada"))).toEqual(ada);
77
- expect(yield* client.listNotes().pipe(as("ada"))).toEqual({
78
- notes: [
79
- { id: ada.id, title: "Plan" },
80
- { id: another.id, title: "Plan" },
81
- ],
82
- });
83
- expect(yield* client.listNotes().pipe(as("grace"))).toEqual({
84
- notes: [{ id: grace.id, title: "Plan" }],
85
- });
86
- }).pipe(Effect.provide(NoteStore.layerMemory)),
76
+ it.effect(
77
+ "scopes keys to the caller, and treats a new key as a new creation",
78
+ () =>
79
+ Effect.gen(function* () {
80
+ const client = yield* Action.client(notes);
81
+ const input = { requestId: "plan", title: "Plan", body: "Draft" };
82
+ const ada = yield* client.createNote(input).pipe(as("ada"));
83
+
84
+ const grace = yield* client
85
+ .createNote({ ...input, body: "Grace's draft" })
86
+ .pipe(as("grace"));
87
+
88
+ const another = yield* client
89
+ .createNote({ ...input, requestId: "another" })
90
+ .pipe(as("ada"));
91
+
92
+ expect(new Set([ada.id, grace.id, another.id]).size).toBe(3);
93
+ expect(yield* client.createNote(input).pipe(as("ada"))).toStrictEqual(
94
+ ada
95
+ );
96
+ expect(yield* client.listNotes().pipe(as("ada"))).toStrictEqual({
97
+ notes: [
98
+ { id: ada.id, title: "Plan" },
99
+ { id: another.id, title: "Plan" },
100
+ ],
101
+ });
102
+ expect(yield* client.listNotes().pipe(as("grace"))).toStrictEqual({
103
+ notes: [{ id: grace.id, title: "Plan" }],
104
+ });
105
+ }).pipe(Effect.provide(NoteStore.layerMemory))
87
106
  );
88
107
 
89
- it.effect("retains the original result after the note changes or is deleted", () =>
90
- Effect.gen(function* () {
91
- const client = yield* Action.client(notes);
92
- const input = { requestId: "plan", title: "Plan", body: "Draft" };
93
- const original = yield* client.createNote(input);
108
+ it.effect(
109
+ "retains the original result after the note changes or is deleted",
110
+ () =>
111
+ Effect.gen(function* () {
112
+ const client = yield* Action.client(notes);
113
+ const input = { requestId: "plan", title: "Plan", body: "Draft" };
114
+ const original = yield* client.createNote(input);
94
115
 
95
- yield* client.updateNote({ id: original.id, body: "Final", revision: 1 });
116
+ yield* client.updateNote({ id: original.id, body: "Final", revision: 1 });
96
117
 
97
- expect(yield* client.createNote(input)).toEqual(original);
98
- expect(yield* client.getNote({ id: original.id })).toMatchObject({
99
- body: "Final",
100
- revision: 2,
101
- });
118
+ expect(yield* client.createNote(input)).toStrictEqual(original);
119
+ expect(yield* client.getNote({ id: original.id })).toMatchObject({
120
+ body: "Final",
121
+ revision: 2,
122
+ });
102
123
 
103
- yield* client.deleteNote({ id: original.id });
124
+ yield* client.deleteNote({ id: original.id });
104
125
 
105
- expect(yield* client.createNote(input)).toEqual(original);
106
- expect(yield* client.listNotes()).toEqual({ notes: [] });
107
- }).pipe(as("ada"), Effect.provide(NoteStore.layerMemory)),
126
+ expect(yield* client.createNote(input)).toStrictEqual(original);
127
+ expect(yield* client.listNotes()).toStrictEqual({ notes: [] });
128
+ }).pipe(as("ada"), Effect.provide(NoteStore.layerMemory))
108
129
  );
109
130
 
110
- it.effect("creates one note for concurrent duplicates, including under frequent fiber yields", () =>
111
- Effect.gen(function* () {
112
- const client = yield* Action.client(notes);
113
- const steps = Array.range(3, 60);
114
-
115
- yield* Effect.forEach(steps, (limit) =>
116
- Effect.gen(function* () {
117
- const input = { requestId: `concurrent-${limit}`, title: "Plan", body: "Draft" };
118
-
119
- const created = yield* Effect.all(Array.replicate(client.createNote(input), 3), {
120
- concurrency: "unbounded",
121
- }).pipe(Effect.provideService(References.MaxOpsBeforeYield, limit));
122
-
123
- expect(created).toEqual(Array.replicate(created[0], 3));
124
- }),
125
- );
126
-
127
- expect((yield* client.listNotes()).notes).toHaveLength(steps.length);
128
- }).pipe(as("ada"), Effect.provide(NoteStore.layerMemory)),
131
+ it.effect(
132
+ "creates one note for concurrent duplicates, including under frequent fiber yields",
133
+ () =>
134
+ Effect.gen(function* () {
135
+ const client = yield* Action.client(notes);
136
+ const steps = Array.range(3, 60);
137
+
138
+ yield* Effect.forEach(steps, (limit) =>
139
+ Effect.gen(function* () {
140
+ const input = {
141
+ requestId: `concurrent-${limit}`,
142
+ title: "Plan",
143
+ body: "Draft",
144
+ };
145
+
146
+ const created = yield* Effect.all(
147
+ Array.replicate(client.createNote(input), 3),
148
+ {
149
+ concurrency: "unbounded",
150
+ }
151
+ ).pipe(Effect.provideService(References.MaxOpsBeforeYield, limit));
152
+
153
+ expect(created).toStrictEqual(Array.replicate(created[0], 3));
154
+ })
155
+ );
156
+
157
+ expect((yield* client.listNotes()).notes).toHaveLength(steps.length);
158
+ }).pipe(as("ada"), Effect.provide(NoteStore.layerMemory))
129
159
  );