@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.
- package/README.md +19 -41
- package/dist/Action.d.mts +140 -48
- package/dist/Action.mjs +88 -44
- package/dist/ActionCli.d.mts +70 -35
- package/dist/ActionCli.mjs +53 -34
- package/dist/ActionHttp.d.mts +70 -38
- package/dist/ActionHttp.mjs +76 -25
- package/dist/ActionMcp.d.mts +56 -24
- package/dist/ActionMcp.mjs +104 -61
- package/dist/ActionRpc.d.mts +222 -57
- package/dist/ActionRpc.mjs +301 -49
- package/dist/ActionToolkit.d.mts +22 -7
- package/dist/ActionToolkit.mjs +37 -14
- package/dist/Authentication.d.mts +42 -12
- package/dist/Authentication.mjs +38 -32
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.d.mts +1 -3
- package/dist/Testing.mjs +2 -3
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -12
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -4
- package/dist/cli/commands.d.mts +9 -7
- package/dist/cli/commands.mjs +85 -35
- package/dist/contract/ast.mjs +5 -3
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -8
- package/dist/contract/call.mjs +49 -8
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +12 -3
- package/dist/contract/caller.mjs +23 -5
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +26 -11
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -5
- package/dist/contract/errors.mjs +2 -3
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hints.mjs +1 -2
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -51
- package/dist/contract/implementation.mjs +94 -60
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/media.d.mts +1 -3
- package/dist/contract/media.mjs +1 -2
- package/dist/contract/names.mjs +15 -6
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +15 -7
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -14
- package/dist/http/client.mjs +52 -14
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +2 -3
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -99
- package/dist/mcp/client.mjs +60 -134
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +40 -15
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +14 -7
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +4 -5
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +57 -61
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -53
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +25 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +19 -17
|
@@ -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 {
|
|
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.
|
|
21
|
+
Layer.provide(RpcSerialization.layerNdjson)
|
|
18
22
|
);
|
|
19
23
|
|
|
20
24
|
const served = RpcClient.layerProtocolHttp({ url: "/rpc" }).pipe(
|
|
21
|
-
Layer.provide(RpcSerialization.
|
|
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) =>
|
|
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
|
-
|
|
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(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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
|
-
|
|
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, {
|
|
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, {
|
|
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) =>
|
|
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(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
124
|
+
yield* Effect.promise(async () => await ada.post("Short."));
|
|
66
125
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
105
|
-
|
|
|
106
|
-
| Public tools described for a model, with field descriptions
|
|
107
|
-
| `readOnly`, `title`, `idempotentHint`, and destructive as MCP's default | `src/printers/Printers.ts`
|
|
108
|
-
| One declared error shared by every tool
|
|
109
|
-
| An input a model cannot use to reach another URL
|
|
110
|
-
| A tool answering an image beside other fields
|
|
111
|
-
| An upstream client reading `Config`, signing in once
|
|
112
|
-
| An upstream's failures mapped to the declared error
|
|
113
|
-
| A builder yielding a service, and handlers that only delegate
|
|
114
|
-
| Server `instructions`, shared by both transports
|
|
115
|
-
| Allowed origins from `Config`, their CORS, and `/healthz`
|
|
116
|
-
| A request body limit, and where an unauthenticated server binds
|
|
117
|
-
| A stdio server whose logs go to stderr
|
|
118
|
-
| A fake upstream provided as a Layer, to a test or an entrypoint
|
|
119
|
-
| Testing tools and the upstream's failure messages in process
|
|
120
|
-
| Testing listed hints, an image and the origin check in memory
|
|
121
|
-
| Testing CORS for one configured origin and for several
|
|
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({
|
|
24
|
+
Schema.Struct({
|
|
25
|
+
user: Schema.NonEmptyString,
|
|
26
|
+
password: Schema.NonEmptyString,
|
|
27
|
+
})
|
|
23
28
|
);
|
|
24
29
|
|
|
25
|
-
const Job = Schema.fromJsonString(
|
|
30
|
+
const Job = Schema.fromJsonString(
|
|
31
|
+
Schema.Struct({ file: Schema.NonEmptyString })
|
|
32
|
+
);
|
|
26
33
|
|
|
27
|
-
const refuse = (status: number, error: string) =>
|
|
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
|
-
[
|
|
41
|
-
|
|
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.
|
|
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}`)
|
|
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:
|
|
76
|
+
return Response.json({ printers: [...printers.values()] });
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
const [id = "", action = ""] = path
|
|
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)
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
case "
|
|
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.
|
|
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
|
-
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case "DELETE job": {
|
|
98
129
|
return becomes("idle", null);
|
|
99
|
-
|
|
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(
|
|
145
|
+
answer(
|
|
146
|
+
request.method,
|
|
147
|
+
url.pathname,
|
|
148
|
+
request.headers["authorization"],
|
|
149
|
+
body
|
|
150
|
+
)
|
|
112
151
|
);
|
|
113
|
-
})
|
|
152
|
+
})
|
|
114
153
|
);
|
|
115
154
|
});
|