@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.
- package/README.md +19 -41
- package/dist/Action.d.mts +140 -46
- package/dist/Action.mjs +87 -42
- package/dist/ActionCli.d.mts +69 -33
- package/dist/ActionCli.mjs +51 -32
- package/dist/ActionHttp.d.mts +69 -35
- package/dist/ActionHttp.mjs +75 -23
- package/dist/ActionMcp.d.mts +56 -22
- package/dist/ActionMcp.mjs +94 -69
- package/dist/ActionRpc.d.mts +222 -55
- package/dist/ActionRpc.mjs +299 -46
- package/dist/ActionToolkit.d.mts +22 -5
- package/dist/ActionToolkit.mjs +36 -12
- package/dist/Authentication.d.mts +42 -10
- package/dist/Authentication.mjs +37 -30
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.mjs +1 -1
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -10
- package/dist/authentication/provider.mjs +1 -1
- package/dist/authentication/refusal.mjs +2 -2
- package/dist/cli/commands.d.mts +9 -5
- package/dist/cli/commands.mjs +84 -33
- package/dist/contract/ast.mjs +4 -1
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -6
- package/dist/contract/call.mjs +48 -6
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +13 -2
- package/dist/contract/caller.mjs +22 -3
- 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.mjs +25 -9
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -3
- package/dist/contract/errors.mjs +1 -1
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- 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 -47
- package/dist/contract/implementation.mjs +92 -58
- 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/names.mjs +14 -4
- 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 +14 -5
- 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 -12
- package/dist/http/client.mjs +51 -12
- 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 +1 -1
- 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 -97
- package/dist/mcp/client.mjs +59 -132
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +39 -13
- 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 +13 -5
- package/dist/testing/memory.d.mts +1 -1
- package/dist/testing/memory.mjs +3 -3
- 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 +52 -60
- 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 -52
- 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 +21 -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 +108 -47
- 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 +10 -4
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest";
|
|
2
|
-
import { Effect, flow, Layer } from "effect";
|
|
3
|
-
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
4
2
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
5
3
|
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
6
4
|
import * as ActionMcp from "@gjermundgaraba/effect-actions/ActionMcp";
|
|
7
5
|
import * as Testing from "@gjermundgaraba/effect-actions/Testing";
|
|
6
|
+
import { Effect, flow, Layer } from "effect";
|
|
7
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
8
|
+
|
|
8
9
|
import { Tools } from "../src/Actions.js";
|
|
9
10
|
import { AdminHttp, Http } from "../src/Api.js";
|
|
10
|
-
import { memory } from "../src/memory.js";
|
|
11
11
|
import { adminRoutes, builders, publicRoutes } from "../src/routes.js";
|
|
12
|
+
import { services } from "./support/services.js";
|
|
12
13
|
|
|
13
14
|
// The host's surfaces, as routes.ts builds them, answering in memory: no port is opened. Each
|
|
14
15
|
// port is a `Testing.layer` of its own, and what they share is provided above both, as
|
|
15
16
|
// server.ts does.
|
|
16
|
-
const shared = builders.pipe(Layer.provideMerge(
|
|
17
|
+
const shared = builders.pipe(Layer.provideMerge(services()));
|
|
17
18
|
|
|
18
19
|
const onPublicPort = Effect.provide(Testing.layer(publicRoutes));
|
|
19
20
|
|
|
@@ -21,59 +22,92 @@ const onAdminPort = Effect.provide(Testing.layer(adminRoutes));
|
|
|
21
22
|
|
|
22
23
|
const ada = {
|
|
23
24
|
transformClient: HttpClient.mapRequest(
|
|
24
|
-
flow(
|
|
25
|
+
flow(
|
|
26
|
+
HttpClientRequest.bearerToken("ada"),
|
|
27
|
+
HttpClientRequest.setHeader("x-workspace", "acme")
|
|
28
|
+
)
|
|
25
29
|
),
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
const ops = {
|
|
29
|
-
transformClient: HttpClient.mapRequest(
|
|
33
|
+
transformClient: HttpClient.mapRequest(
|
|
34
|
+
HttpClientRequest.setHeader("x-operator-key", "ops-key")
|
|
35
|
+
),
|
|
30
36
|
};
|
|
31
37
|
|
|
32
38
|
const post = (path: string) =>
|
|
33
|
-
HttpClient.execute(
|
|
39
|
+
HttpClient.execute(
|
|
40
|
+
HttpClientRequest.post(path).pipe(HttpClientRequest.bodyJsonUnsafe({}))
|
|
41
|
+
);
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
const from = (url: string, origin: string) =>
|
|
44
|
+
HttpClient.execute(
|
|
45
|
+
ActionMcp.request("tools/list", {}, { url, headers: { origin } })
|
|
46
|
+
);
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
describe("the public port", () => {
|
|
49
|
+
it.effect(
|
|
50
|
+
"answers the public action without a token and refuses the rest",
|
|
51
|
+
() =>
|
|
52
|
+
Effect.gen(function* () {
|
|
53
|
+
const anyone = yield* ActionHttp.client(Http);
|
|
54
|
+
|
|
55
|
+
expect(yield* anyone.getStatus()).toMatchObject({
|
|
56
|
+
service: "tracker",
|
|
57
|
+
workspaces: 2,
|
|
58
|
+
});
|
|
59
|
+
expect(yield* Effect.flip(anyone.whoAmI())).toBeInstanceOf(
|
|
60
|
+
Action.Unauthenticated
|
|
61
|
+
);
|
|
62
|
+
}).pipe(onPublicPort, Effect.provide(shared))
|
|
43
63
|
);
|
|
44
64
|
|
|
45
|
-
it.effect(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
it.effect(
|
|
66
|
+
"lists the tools to a signed-out MCP client, the unlisted action left out",
|
|
67
|
+
() =>
|
|
68
|
+
Effect.gen(function* () {
|
|
69
|
+
const listing = yield* HttpClient.execute(
|
|
70
|
+
ActionMcp.request("tools/list")
|
|
71
|
+
);
|
|
49
72
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
const text = yield* listing.text;
|
|
74
|
+
|
|
75
|
+
expect(listing.status).toBe(200);
|
|
76
|
+
expect(text).toContain('"name":"createIssue"');
|
|
77
|
+
expect(text).not.toContain('"name":"attachImage"');
|
|
78
|
+
}).pipe(onPublicPort, Effect.provide(shared))
|
|
54
79
|
);
|
|
55
80
|
|
|
56
|
-
it.effect(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
it.effect(
|
|
82
|
+
"serves one store to both surfaces: an issue filed over HTTP is read as a tool",
|
|
83
|
+
() =>
|
|
84
|
+
Effect.gen(function* () {
|
|
85
|
+
const http = yield* ActionHttp.client(Http, ada);
|
|
86
|
+
const mcp = yield* ActionMcp.client(Tools, ada);
|
|
61
87
|
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
const issue = yield* http.createIssue({
|
|
89
|
+
title: "Crash",
|
|
90
|
+
body: "On save.",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(yield* mcp.getIssue({ id: issue.id })).toStrictEqual(issue);
|
|
94
|
+
}).pipe(onPublicPort, Effect.provide(shared))
|
|
64
95
|
);
|
|
65
96
|
|
|
66
|
-
it.effect(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
97
|
+
it.effect(
|
|
98
|
+
"refuses, at the MCP endpoint, a page of this host that is not on its list",
|
|
99
|
+
() =>
|
|
100
|
+
Effect.gen(function* () {
|
|
101
|
+
// The request policy lets both through: one is the request's own origin, one is listed.
|
|
102
|
+
const listed = yield* from("/mcp", "http://localhost:6274");
|
|
71
103
|
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
const sameHost = yield* from(
|
|
105
|
+
"http://127.0.0.1/mcp",
|
|
106
|
+
"http://127.0.0.1"
|
|
107
|
+
);
|
|
74
108
|
|
|
75
|
-
|
|
76
|
-
|
|
109
|
+
expect([listed.status, sameHost.status]).toStrictEqual([200, 403]);
|
|
110
|
+
}).pipe(onPublicPort, Effect.provide(shared))
|
|
77
111
|
);
|
|
78
112
|
});
|
|
79
113
|
|
|
@@ -83,42 +117,53 @@ describe("the two ports", () => {
|
|
|
83
117
|
const admin = yield* post("/admin/listWorkspaces").pipe(onPublicPort);
|
|
84
118
|
const member = yield* post("/api/v1/whoAmI").pipe(onAdminPort);
|
|
85
119
|
|
|
86
|
-
expect([admin.status, member.status]).
|
|
87
|
-
}).pipe(Effect.provide(shared))
|
|
120
|
+
expect([admin.status, member.status]).toStrictEqual([404, 404]);
|
|
121
|
+
}).pipe(Effect.provide(shared))
|
|
88
122
|
);
|
|
89
123
|
|
|
90
124
|
it.effect("run a builder once for an implementation both serve", () =>
|
|
91
125
|
Effect.gen(function* () {
|
|
92
|
-
const onPublic = yield* Effect.flatMap(
|
|
93
|
-
client
|
|
126
|
+
const onPublic = yield* Effect.flatMap(
|
|
127
|
+
ActionHttp.client(Http),
|
|
128
|
+
(client) => client.getStatus()
|
|
94
129
|
).pipe(onPublicPort);
|
|
95
130
|
|
|
96
|
-
const onAdmin = yield* Effect.flatMap(
|
|
97
|
-
client
|
|
131
|
+
const onAdmin = yield* Effect.flatMap(
|
|
132
|
+
ActionHttp.client(AdminHttp),
|
|
133
|
+
(client) => client.getStatus()
|
|
98
134
|
).pipe(onAdminPort);
|
|
99
135
|
|
|
100
136
|
// `status`'s builder names the start it ran in. With `builders` above both ports it ran
|
|
101
137
|
// once. Provide `memory` alone instead, and each port runs it: two instances.
|
|
102
138
|
expect(onAdmin.instance).toBe(onPublic.instance);
|
|
103
|
-
}).pipe(Effect.provide(shared))
|
|
139
|
+
}).pipe(Effect.provide(shared))
|
|
104
140
|
);
|
|
105
141
|
|
|
106
|
-
it.effect(
|
|
107
|
-
|
|
108
|
-
|
|
142
|
+
it.effect(
|
|
143
|
+
"share their services: a workspace suspended on one is refused on the other",
|
|
144
|
+
() =>
|
|
145
|
+
Effect.gen(function* () {
|
|
146
|
+
const whoAmI = Effect.flatMap(ActionHttp.client(Http, ada), (client) =>
|
|
147
|
+
client.whoAmI()
|
|
148
|
+
);
|
|
109
149
|
|
|
110
|
-
|
|
150
|
+
expect(yield* whoAmI.pipe(onPublicPort)).toMatchObject({ id: "ada" });
|
|
111
151
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
152
|
+
const suspended = yield* Effect.flatMap(
|
|
153
|
+
ActionHttp.client(AdminHttp, ops),
|
|
154
|
+
(client) => client.suspendWorkspace({ workspace: "acme" })
|
|
155
|
+
).pipe(onAdminPort);
|
|
115
156
|
|
|
116
|
-
|
|
157
|
+
expect(suspended).toStrictEqual({
|
|
158
|
+
slug: "acme",
|
|
159
|
+
name: "Acme",
|
|
160
|
+
suspended: true,
|
|
161
|
+
});
|
|
117
162
|
|
|
118
|
-
|
|
163
|
+
const refused = yield* Effect.flip(whoAmI).pipe(onPublicPort);
|
|
119
164
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
165
|
+
expect(refused).toBeInstanceOf(Action.Forbidden);
|
|
166
|
+
expect(refused.message).toBe("This workspace is suspended.");
|
|
167
|
+
}).pipe(Effect.provide(shared))
|
|
123
168
|
);
|
|
124
169
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
import { CurrentMember } from "../../src/identity/Identity.js";
|
|
4
|
+
import { members } from "../../src/identity/members.js";
|
|
5
|
+
|
|
6
|
+
// What the implementation does, with no transport: `Action.client` runs the same decoding,
|
|
7
|
+
// `authorize` and handler a server runs. Each call or watch names its caller, as a request would.
|
|
8
|
+
export const as = (member: keyof typeof members) =>
|
|
9
|
+
Effect.provideService(CurrentMember, members[member]);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Effect, Layer, Ref, Stream } from "effect";
|
|
2
|
-
import { AiError, LanguageModel
|
|
2
|
+
import { AiError, LanguageModel } from "effect/ai";
|
|
3
|
+
import type { Response } from "effect/ai";
|
|
3
4
|
|
|
4
|
-
type Reply =
|
|
5
|
+
type Reply = readonly Response.PartEncoded[];
|
|
5
6
|
|
|
6
7
|
// A model for a test: it answers its first request with the first reply it was given, its
|
|
7
8
|
// second with the second, whatever the prompt says. A reply is the parts a provider sends, or
|
|
@@ -24,11 +25,11 @@ export const scripted = (
|
|
|
24
25
|
return Effect.isEffect(reply)
|
|
25
26
|
? Effect.map(reply, (parts) => [...parts])
|
|
26
27
|
: Effect.succeed([...reply]);
|
|
27
|
-
}
|
|
28
|
+
}
|
|
28
29
|
),
|
|
29
30
|
streamText: () => Stream.empty,
|
|
30
31
|
});
|
|
31
|
-
})
|
|
32
|
+
})
|
|
32
33
|
);
|
|
33
34
|
|
|
34
35
|
export const down: Effect.Effect<never, AiError.AiError> = Effect.fail(
|
|
@@ -36,5 +37,5 @@ export const down: Effect.Effect<never, AiError.AiError> = Effect.fail(
|
|
|
36
37
|
module: "scripted",
|
|
37
38
|
method: "generateText",
|
|
38
39
|
reason: new AiError.UnknownError({ description: "The provider is down." }),
|
|
39
|
-
})
|
|
40
|
+
})
|
|
40
41
|
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Testing from "@gjermundgaraba/effect-actions/Testing";
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
import { Dns } from "effect/net";
|
|
4
|
+
|
|
5
|
+
import { memory } from "../../src/memory.js";
|
|
6
|
+
|
|
7
|
+
const unreached = async () => new Response(null, { status: 503 });
|
|
8
|
+
|
|
9
|
+
// What the public routes need, in memory: the application's services, and for webhook delivery
|
|
10
|
+
// a client and a resolver. `subscribers` answers each POST in place of a member's server, and
|
|
11
|
+
// hooks.example.com resolves to a public address: delivery refuses a private or loopback one.
|
|
12
|
+
export const services = (subscribers = Testing.layer(unreached)) =>
|
|
13
|
+
Layer.mergeAll(
|
|
14
|
+
memory,
|
|
15
|
+
subscribers,
|
|
16
|
+
Dns.layerStatic({ hosts: { "hooks.example.com": ["93.184.216.34"] } })
|
|
17
|
+
);
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
# live-rpc
|
|
2
2
|
|
|
3
|
-
A chat served as Effect RPC over a WebSocket: one connection per page, many calls on it.
|
|
4
|
-
Members post, read and delete messages in the rooms they belong to; anyone may list the rooms.
|
|
5
|
-
Each message on the socket carries its own token and names its room.
|
|
6
|
-
To serve these same actions as JSON too, see
|
|
7
|
-
[ActionRpc.md](../../docs/ActionRpc.md#serve-the-rpcs).
|
|
3
|
+
A chat served as Effect RPC over a WebSocket: one connection per page, many calls on it. Members post, read and delete messages in the rooms they belong to, and watch a room, each post reaching their page as it is made; anyone may list the rooms. Each message on the socket carries its own token and names its room. To serve these same actions as JSON too, see [ActionRpc.md](../../docs/ActionRpc.md#serve-the-rpcs).
|
|
8
4
|
|
|
9
5
|
## Run it
|
|
10
6
|
|
|
11
|
-
From the repository root, after `vp install` (from an installed package, see
|
|
12
|
-
[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)):
|
|
13
8
|
|
|
14
9
|
```sh
|
|
15
10
|
node --import tsx examples/live-rpc/src/server.ts # 127.0.0.1:3000, HOST and PORT to change
|
|
@@ -18,54 +13,57 @@ node --import tsx examples/live-rpc/src/client.ts # CHAT_URL to change, ws:/
|
|
|
18
13
|
vp test examples/live-rpc
|
|
19
14
|
```
|
|
20
15
|
|
|
21
|
-
`client.ts` runs the browser's client from Node: two members on one socket, then each declared
|
|
22
|
-
error and refusal a page handles. `browser.ts` is imported by a page, not run: `openChat` for a
|
|
23
|
-
page that is an Effect program; `page.ts` wraps it as `connect(url)` for one that calls with promises.
|
|
16
|
+
`client.ts` runs the browser's client from Node: two members on one socket, a watch of the room, then each declared error and refusal a page handles. `browser.ts` is imported by a page, not run: `openChat` for a page that is an Effect program; `page.ts` wraps it as `connect(url)` for one that calls with promises.
|
|
24
17
|
|
|
25
18
|
## Layout
|
|
26
19
|
|
|
27
20
|
```text
|
|
28
21
|
src/
|
|
29
22
|
├── chat/
|
|
30
|
-
│ ├── Chat.ts contract: schemas, errors, actions, the room header and its default
|
|
23
|
+
│ ├── Chat.ts contract: schemas, errors, actions, MessagePosted, the room header and its default
|
|
31
24
|
│ ├── store.ts MessageStore, the service the handlers call, and its Layer
|
|
32
25
|
│ ├── room.ts CurrentRoom, and the RPC middleware providing it per message
|
|
33
26
|
│ ├── authorize.ts the one authorization rule: a member of the room
|
|
34
|
-
│
|
|
27
|
+
│ ├── handlers.ts Action.implement: contract to store, a post notifying its room
|
|
28
|
+
│ └── events.ts Event.implement: MessagePosted read from the store, keyed by room
|
|
35
29
|
├── identity/
|
|
36
30
|
│ ├── Identity.ts contract: CurrentMember and the Login descriptor
|
|
37
31
|
│ ├── members.ts the demo members
|
|
38
32
|
│ └── verifier.ts Authentication.layer: tokens to members, server only
|
|
39
|
-
├── Rpc.ts the RPC binding of every action
|
|
33
|
+
├── Rpc.ts the RPC binding of every action and MessagePosted
|
|
40
34
|
├── routes.ts the rpcs, authenticated and with their middleware, not yet on a protocol
|
|
41
|
-
├── server.ts entrypoint: the rpcs over a WebSocket at /rpc, its messages bounded
|
|
35
|
+
├── server.ts entrypoint: the rpcs over a WebSocket at /rpc, its messages bounded, one Events
|
|
42
36
|
├── browser.ts client module: the connection and the chat, as one Effect program
|
|
43
37
|
├── page.ts the same chat as promises, for a page that is not one
|
|
44
38
|
└── client.ts entrypoint: that client, run from Node
|
|
45
39
|
test/
|
|
46
|
-
├── chat/ in process, with Action.client
|
|
47
|
-
├── routes.test.ts over RPC in memory, with Testing.layer
|
|
40
|
+
├── chat/ in process, with Action.client, a watch included
|
|
41
|
+
├── routes.test.ts over RPC in memory, with Testing.layer, a watch over NDJSON
|
|
48
42
|
├── browser.test.ts the page's client against the same rpcs
|
|
49
|
-
└── server.test.ts the socket itself: a listening port, connect(url), the bound on a message
|
|
43
|
+
└── server.test.ts the socket itself: a listening port, connect(url), a watch, the bound on a message
|
|
50
44
|
```
|
|
51
45
|
|
|
52
46
|
## Where to look
|
|
53
47
|
|
|
54
|
-
| To see
|
|
55
|
-
|
|
|
56
|
-
| An RPC binding naming its authentication descriptor
|
|
57
|
-
| The rpcs on Effect's `RpcServer`, before a protocol is chosen
|
|
58
|
-
| Serving them over a WebSocket, speaking JSON
|
|
59
|
-
| A verifier that runs for every message, not once per connection
|
|
60
|
-
| Native `RpcMiddleware` providing a per-request service, with a default | `src/chat/room.ts`
|
|
61
|
-
| Handlers reading that service, and the rule that reads it
|
|
62
|
-
| A public rpc beside protected ones
|
|
63
|
-
| A socket client sending its token and room on each message
|
|
64
|
-
| Who owns the socket, and how a promise page keeps it open
|
|
65
|
-
| A declared error and the built-in refusals handled by tag
|
|
66
|
-
| Running that client from Node, two callers on one socket
|
|
67
|
-
| Testing handlers and authorization with no transport
|
|
68
|
-
| Testing rpcs, authentication and middleware in memory
|
|
69
|
-
| The binding's native `RpcGroup` called by Effect's own client
|
|
70
|
-
| Testing the client's handling of each error and refusal
|
|
71
|
-
| Testing over a real WebSocket, and a message over `maxPayload`
|
|
48
|
+
| To see | Read |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| An RPC binding naming its authentication descriptor | `src/Rpc.ts` |
|
|
51
|
+
| The rpcs on Effect's `RpcServer`, before a protocol is chosen | `src/routes.ts` |
|
|
52
|
+
| Serving them over a WebSocket, speaking JSON | `src/server.ts` |
|
|
53
|
+
| A verifier that runs for every message, not once per connection | `src/identity/verifier.ts` |
|
|
54
|
+
| Native `RpcMiddleware` providing a per-request service, with a default | `src/chat/room.ts` |
|
|
55
|
+
| Handlers reading that service, and the rule that reads it | `src/chat/handlers.ts`, `src/chat/authorize.ts` |
|
|
56
|
+
| A public rpc beside protected ones | `src/chat/Chat.ts` |
|
|
57
|
+
| A socket client sending its token and room on each message | `src/browser.ts` |
|
|
58
|
+
| Who owns the socket, and how a promise page keeps it open | `src/browser.ts`, `src/page.ts` |
|
|
59
|
+
| A declared error and the built-in refusals handled by tag | `src/browser.ts` |
|
|
60
|
+
| Running that client from Node, two callers on one socket | `src/client.ts` |
|
|
61
|
+
| Testing handlers and authorization with no transport | `test/chat/chat.test.ts` |
|
|
62
|
+
| Testing rpcs, authentication and middleware in memory | `test/routes.test.ts` |
|
|
63
|
+
| The binding's native `RpcGroup` called by Effect's own client | `test/routes.test.ts` |
|
|
64
|
+
| Testing the client's handling of each error and refusal | `test/browser.test.ts` |
|
|
65
|
+
| Testing over a real WebSocket, and a message over `maxPayload` | `test/server.test.ts` |
|
|
66
|
+
| An event read from the store, its `key` the room a header names | `src/chat/events.ts` |
|
|
67
|
+
| A handler notifying the room it posted to | `src/chat/handlers.ts` |
|
|
68
|
+
| A page watching a room, its headers on the stream, across drops | `src/browser.ts`, `src/page.ts` |
|
|
69
|
+
| Testing a watch in process, over RPC, and over a real WebSocket | `test/chat/chat.test.ts`, `test/routes.test.ts`, `test/server.test.ts` |
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { ListRooms, MessagePosted, Messages } from "./chat/Chat.js";
|
|
3
4
|
import { Login } from "./identity/Identity.js";
|
|
4
5
|
|
|
5
|
-
// The RPC binding: one native rpc per action, named after it,
|
|
6
|
-
// `Login`. It is plain data and imports no server
|
|
7
|
-
// import this one value. `Rpc.group` is the native
|
|
8
|
-
|
|
6
|
+
// The RPC binding: one native rpc per action, named after it, and a stream rpc for
|
|
7
|
+
// `MessagePosted`, the protected ones requiring `Login`. It is plain data and imports no server
|
|
8
|
+
// code, so the server and a browser page both import this one value. `Rpc.group` is the native
|
|
9
|
+
// `RpcGroup`, for Effect's own `RpcClient.make`.
|
|
10
|
+
export const Rpc = ActionRpc.make([ListRooms, ...Messages, MessagePosted], {
|
|
11
|
+
authentication: Login,
|
|
12
|
+
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
|
|
2
|
+
import { Effect, Layer, Stream } from "effect";
|
|
3
|
+
import { Headers } from "effect/http";
|
|
2
4
|
import { RpcClient, RpcSerialization } from "effect/rpc";
|
|
3
5
|
import { Socket } from "effect/socket";
|
|
4
|
-
|
|
5
|
-
import { roomHeader } from "./chat/Chat.js";
|
|
6
|
+
|
|
7
|
+
import { MessagePosted, roomHeader } from "./chat/Chat.js";
|
|
6
8
|
import { Rpc } from "./Rpc.js";
|
|
7
9
|
|
|
8
10
|
// Client module: what a web page imports. It holds the binding and nothing of the server, so a
|
|
@@ -15,7 +17,7 @@ import { Rpc } from "./Rpc.js";
|
|
|
15
17
|
export const connection = (url: string) =>
|
|
16
18
|
RpcClient.layerProtocolSocket().pipe(
|
|
17
19
|
Layer.provide(Socket.layerWebSocket(url)),
|
|
18
|
-
Layer.provide(RpcSerialization.layerJson)
|
|
20
|
+
Layer.provide(RpcSerialization.layerJson)
|
|
19
21
|
);
|
|
20
22
|
|
|
21
23
|
// The chat as a page uses it, on the connection provided around it.
|
|
@@ -30,24 +32,50 @@ export const openChat = Effect.gen(function* () {
|
|
|
30
32
|
room: (name: string, token: string) => {
|
|
31
33
|
// A browser WebSocket cannot set an `Authorization` header on its upgrade, so the token
|
|
32
34
|
// travels on each message, and the room beside it. A refreshed token needs no new socket.
|
|
33
|
-
const
|
|
35
|
+
const headers = { authorization: `Bearer ${token}`, [roomHeader]: name };
|
|
36
|
+
const here = RpcClient.withHeaders(headers);
|
|
34
37
|
|
|
35
38
|
return {
|
|
36
|
-
//
|
|
37
|
-
//
|
|
39
|
+
// What the room holds after the last id the page has. A token may expire while the
|
|
40
|
+
// page stays open, so each refusal becomes what the page shows.
|
|
38
41
|
read: (after: number) =>
|
|
39
42
|
client.readMessages({ after }).pipe(
|
|
40
43
|
here,
|
|
41
|
-
Effect.map(({ messages }) => ({
|
|
44
|
+
Effect.map(({ messages }) => ({
|
|
45
|
+
access: "member" as const,
|
|
46
|
+
messages,
|
|
47
|
+
})),
|
|
42
48
|
Effect.catchTags({
|
|
43
49
|
Unauthenticated: () =>
|
|
44
50
|
Effect.succeed({ access: "signed-out" as const, messages: [] }),
|
|
45
|
-
Forbidden: () =>
|
|
46
|
-
|
|
51
|
+
Forbidden: () =>
|
|
52
|
+
Effect.succeed({
|
|
53
|
+
access: "not-a-member" as const,
|
|
54
|
+
messages: [],
|
|
55
|
+
}),
|
|
56
|
+
})
|
|
47
57
|
),
|
|
48
58
|
|
|
49
59
|
post: (text: string) => client.postMessage({ text }).pipe(here),
|
|
50
60
|
|
|
61
|
+
// Each message posted to the room after the last id the page holds: a page opening a
|
|
62
|
+
// room reads it, then watches from the last id it read, so a post between the two is
|
|
63
|
+
// not lost; 0 sends the room's history first. `watch` connects again after a drop and
|
|
64
|
+
// resumes from the last message it delivered, so the page never subscribes by hand; a
|
|
65
|
+
// `Gap` says some were missed, or are another run's, so the page reads the room again
|
|
66
|
+
// with `read(0)`. The headers go with the
|
|
67
|
+
// stream, as `here` puts them on a call, and so with every request it sends again. A
|
|
68
|
+
// refusal ends it, as when it connects again with a token that has expired.
|
|
69
|
+
messages: (after: number) =>
|
|
70
|
+
client
|
|
71
|
+
.watch(MessagePosted, {}, { cursor: String(after) })
|
|
72
|
+
.pipe(
|
|
73
|
+
Stream.updateService(
|
|
74
|
+
RpcClient.CurrentHeaders,
|
|
75
|
+
Headers.merge(Headers.fromInput(headers))
|
|
76
|
+
)
|
|
77
|
+
),
|
|
78
|
+
|
|
51
79
|
// A declared error arrives as the contract's class. A message someone else already
|
|
52
80
|
// removed is gone, which is what the page asked for.
|
|
53
81
|
remove: (id: number) =>
|
|
@@ -57,7 +85,7 @@ export const openChat = Effect.gen(function* () {
|
|
|
57
85
|
Effect.catchTags({
|
|
58
86
|
MessageNotFound: () => Effect.succeed("removed" as const),
|
|
59
87
|
NotAuthor: () => Effect.succeed("not-yours" as const),
|
|
60
|
-
})
|
|
88
|
+
})
|
|
61
89
|
),
|
|
62
90
|
};
|
|
63
91
|
},
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import * as Event from "@gjermundgaraba/effect-actions/Event";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
|
|
3
5
|
import { CurrentMember } from "../identity/Identity.js";
|
|
4
6
|
|
|
5
7
|
// A message's `id` counts up within its room, so a client asks for what came after the last
|
|
@@ -15,19 +17,17 @@ export type Message = typeof Message.Type;
|
|
|
15
17
|
// A declared error is part of the contract: every client decodes it as this class. Its status
|
|
16
18
|
// is for a JSON binding, should these actions be served as one too: an rpc carries no status,
|
|
17
19
|
// and an error without one answers 422 over HTTP.
|
|
18
|
-
export class MessageNotFound extends Schema.TaggedError<
|
|
19
|
-
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
) {}
|
|
20
|
+
export class MessageNotFound extends Schema.TaggedError<
|
|
21
|
+
MessageNotFound,
|
|
22
|
+
{ readonly brand: unique symbol }
|
|
23
|
+
>()("MessageNotFound", { id: Schema.Int }, { httpApiStatus: 404 }) {}
|
|
23
24
|
|
|
24
25
|
// A refusal carries what the caller sent and no more: who wrote the message is for the actions
|
|
25
26
|
// that read it, under their own rule.
|
|
26
|
-
export class NotAuthor extends Schema.TaggedError<
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
) {}
|
|
27
|
+
export class NotAuthor extends Schema.TaggedError<
|
|
28
|
+
NotAuthor,
|
|
29
|
+
{ readonly brand: unique symbol }
|
|
30
|
+
>()("NotAuthor", { id: Schema.Int }, { httpApiStatus: 403 }) {}
|
|
31
31
|
|
|
32
32
|
// Which room a call is about is not an input of any action: it travels beside the call, in
|
|
33
33
|
// this header, as the token does, because `authorize` sees the action and not its input, and
|
|
@@ -44,9 +44,12 @@ export const defaultRoom = "lobby";
|
|
|
44
44
|
// tells a signed-out caller every room's name and how many messages it holds, a room appearing
|
|
45
45
|
// once someone posts: decide whether yours may.
|
|
46
46
|
export const ListRooms = Action.make("listRooms", {
|
|
47
|
-
description:
|
|
47
|
+
description:
|
|
48
|
+
"List the rooms someone has posted in, and how many messages each holds.",
|
|
48
49
|
success: {
|
|
49
|
-
rooms: Schema.Array(
|
|
50
|
+
rooms: Schema.Array(
|
|
51
|
+
Schema.Struct({ name: Schema.String, messages: Schema.Int })
|
|
52
|
+
),
|
|
50
53
|
},
|
|
51
54
|
readOnly: true,
|
|
52
55
|
caller: Action.Anyone,
|
|
@@ -54,8 +57,8 @@ export const ListRooms = Action.make("listRooms", {
|
|
|
54
57
|
|
|
55
58
|
const signedIn = { caller: CurrentMember } as const;
|
|
56
59
|
|
|
57
|
-
//
|
|
58
|
-
//
|
|
60
|
+
// Reads once: a page opening a room reads what is there, then watches `MessagePosted` below from
|
|
61
|
+
// the last id it read.
|
|
59
62
|
export const ReadMessages = Action.make("readMessages", {
|
|
60
63
|
...signedIn,
|
|
61
64
|
description: "Read the room's messages after the given id; 0 reads them all.",
|
|
@@ -67,7 +70,9 @@ export const ReadMessages = Action.make("readMessages", {
|
|
|
67
70
|
export const PostMessage = Action.make("postMessage", {
|
|
68
71
|
...signedIn,
|
|
69
72
|
description: "Post a message to the room.",
|
|
70
|
-
input: {
|
|
73
|
+
input: {
|
|
74
|
+
text: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(500)),
|
|
75
|
+
},
|
|
71
76
|
success: Message,
|
|
72
77
|
readOnly: false,
|
|
73
78
|
});
|
|
@@ -81,3 +86,12 @@ export const DeleteMessage = Action.make("deleteMessage", {
|
|
|
81
86
|
});
|
|
82
87
|
|
|
83
88
|
export const Messages = [ReadMessages, PostMessage, DeleteMessage] as const;
|
|
89
|
+
|
|
90
|
+
// Each message posted to the room, as it is posted: a page watches it on its open socket,
|
|
91
|
+
// about the room its header names, as the actions are. Signed-in, so each watch authenticates
|
|
92
|
+
// and runs `authorize` as it starts, refused for a member outside the room.
|
|
93
|
+
export const MessagePosted = Event.make("message.posted", {
|
|
94
|
+
description: "A message posted to the room.",
|
|
95
|
+
payload: Message,
|
|
96
|
+
caller: CurrentMember,
|
|
97
|
+
});
|