@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,16 +1,19 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest";
|
|
2
|
-
import { Effect, Layer } from "effect";
|
|
3
2
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
4
|
-
import {
|
|
3
|
+
import { Effect, Layer } from "effect";
|
|
4
|
+
|
|
5
|
+
import { CurrentCaller } from "../../src/identity/Identity.js";
|
|
6
|
+
import type { Member, Operator } from "../../src/identity/Identity.js";
|
|
5
7
|
import { members } from "../../src/identity/members.js";
|
|
6
8
|
import { Permissions } from "../../src/identity/permissions.js";
|
|
7
9
|
import { tasks } from "../../src/tasks/handlers.js";
|
|
8
|
-
import { TaskNotFound } from "../../src/tasks/Tasks.js";
|
|
9
10
|
import { TaskStore } from "../../src/tasks/store.js";
|
|
11
|
+
import { TaskNotFound } from "../../src/tasks/Tasks.js";
|
|
10
12
|
|
|
11
13
|
// What the implementation does, with no model and no command line: `Action.client` runs the
|
|
12
14
|
// same decoding, `authorize` and handler every surface runs. Each call names its caller.
|
|
13
|
-
const as = (caller: Member | Operator) =>
|
|
15
|
+
const as = (caller: Member | Operator) =>
|
|
16
|
+
Effect.provideService(CurrentCaller, caller);
|
|
14
17
|
|
|
15
18
|
const operator: Operator = { kind: "operator", id: "root" };
|
|
16
19
|
|
|
@@ -22,52 +25,77 @@ describe("tasks, in process", () => {
|
|
|
22
25
|
Effect.gen(function* () {
|
|
23
26
|
const client = yield* Action.client(tasks);
|
|
24
27
|
|
|
25
|
-
yield* client
|
|
26
|
-
|
|
28
|
+
yield* client
|
|
29
|
+
.addTask({ title: "Book the venue", assignee: "ada" })
|
|
30
|
+
.pipe(as(members.ada));
|
|
31
|
+
yield* client
|
|
32
|
+
.addTask({ title: "Send invitations", assignee: "grace" })
|
|
33
|
+
.pipe(as(operator));
|
|
27
34
|
|
|
28
|
-
const { tasks: listed } = yield* client
|
|
35
|
+
const { tasks: listed } = yield* client
|
|
36
|
+
.listTasks()
|
|
37
|
+
.pipe(as(members.grace));
|
|
29
38
|
|
|
30
|
-
expect(listed.map(({ id, addedBy }) => [id, addedBy])).
|
|
39
|
+
expect(listed.map(({ id, addedBy }) => [id, addedBy])).toStrictEqual([
|
|
31
40
|
["1", "ada"],
|
|
32
41
|
["2", "root"],
|
|
33
42
|
]);
|
|
34
|
-
expect(
|
|
43
|
+
expect(
|
|
44
|
+
yield* client.listTasks({ assignee: "grace" }).pipe(as(members.grace))
|
|
45
|
+
).toMatchObject({
|
|
35
46
|
tasks: [{ title: "Send invitations" }],
|
|
36
47
|
});
|
|
37
|
-
}).pipe(Effect.provide(services))
|
|
48
|
+
}).pipe(Effect.provide(services))
|
|
38
49
|
);
|
|
39
50
|
|
|
40
|
-
it.effect(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
it.effect(
|
|
52
|
+
"refuses a write from a member the store grants reading alone",
|
|
53
|
+
() =>
|
|
54
|
+
Effect.gen(function* () {
|
|
55
|
+
const client = yield* Action.client(tasks);
|
|
56
|
+
const add = client.addTask({ title: "Order lunch", assignee: "guest" });
|
|
44
57
|
|
|
45
|
-
|
|
58
|
+
const refusal = yield* Effect.flip(add.pipe(as(members.guest)));
|
|
46
59
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
expect(refusal).toBeInstanceOf(Action.Forbidden);
|
|
61
|
+
expect(refusal.message).toBe("Requires tasks:write.");
|
|
62
|
+
expect(yield* client.listTasks().pipe(as(members.guest))).toStrictEqual(
|
|
63
|
+
{
|
|
64
|
+
tasks: [],
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}).pipe(Effect.provide(services))
|
|
51
68
|
);
|
|
52
69
|
|
|
53
|
-
it.effect(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
it.effect(
|
|
71
|
+
"admits the operator, whom the permission store does not know",
|
|
72
|
+
() =>
|
|
73
|
+
Effect.gen(function* () {
|
|
74
|
+
const client = yield* Action.client(tasks);
|
|
75
|
+
|
|
76
|
+
const add = client.addTask({
|
|
77
|
+
title: "Rotate the keys",
|
|
78
|
+
assignee: "ada",
|
|
79
|
+
});
|
|
58
80
|
|
|
59
|
-
|
|
60
|
-
|
|
81
|
+
const { id } = yield* add.pipe(as(operator));
|
|
82
|
+
|
|
83
|
+
expect(
|
|
84
|
+
yield* client.completeTask({ id }).pipe(as(operator))
|
|
85
|
+
).toMatchObject({ done: true });
|
|
86
|
+
}).pipe(Effect.provide(services))
|
|
61
87
|
);
|
|
62
88
|
|
|
63
|
-
it.effect(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
it.effect(
|
|
90
|
+
"fails with the declared error for a task that does not exist",
|
|
91
|
+
() =>
|
|
92
|
+
Effect.gen(function* () {
|
|
93
|
+
const client = yield* Action.client(tasks);
|
|
94
|
+
const annotate = client.annotateTask({ id: "9", notes: "Urgent." });
|
|
67
95
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
96
|
+
expect(
|
|
97
|
+
yield* Effect.flip(annotate.pipe(as(members.ada)))
|
|
98
|
+
).toStrictEqual(new TaskNotFound({ id: "9" }));
|
|
99
|
+
}).pipe(Effect.provide(services))
|
|
72
100
|
);
|
|
73
101
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest";
|
|
2
2
|
import { Effect, Layer, Stream } from "effect";
|
|
3
3
|
import { Chat, LanguageModel } from "effect/ai";
|
|
4
|
+
|
|
4
5
|
import type { Member } from "../src/identity/Identity.js";
|
|
5
6
|
import { members } from "../src/identity/members.js";
|
|
6
7
|
import { Permissions } from "../src/identity/permissions.js";
|
|
@@ -11,21 +12,43 @@ import { actingFor, layer, toolkit } from "../src/toolkit.js";
|
|
|
11
12
|
// What the toolkit adds to the implementations: a caller per call, and a model's approvals.
|
|
12
13
|
// The handlers run in this process, on a store in memory the tests can read too.
|
|
13
14
|
const tools = layer.pipe(
|
|
14
|
-
Layer.provideMerge(
|
|
15
|
+
Layer.provideMerge(
|
|
16
|
+
Layer.mergeAll(TaskStore.layerMemory, Permissions.layerMemory)
|
|
17
|
+
)
|
|
15
18
|
);
|
|
16
19
|
|
|
17
20
|
// A model's call of `addTask`, as a provider sends it.
|
|
18
21
|
const adding = (id: string, assignee: string) =>
|
|
19
|
-
({
|
|
22
|
+
({
|
|
23
|
+
type: "tool-call",
|
|
24
|
+
id,
|
|
25
|
+
name: "addTask",
|
|
26
|
+
params: { title: "Order lunch", assignee },
|
|
27
|
+
}) as const;
|
|
20
28
|
|
|
21
29
|
// Which calls of a turn ran, and which wait for a person.
|
|
22
|
-
const outcome = (
|
|
30
|
+
const outcome = (
|
|
31
|
+
response: LanguageModel.GenerateTextResponse<typeof toolkit.tools, "opaque">
|
|
32
|
+
) => ({
|
|
23
33
|
ran: response.toolResults.map(({ id }) => id),
|
|
24
34
|
waiting: response.content.flatMap((part) =>
|
|
25
|
-
part.type === "tool-approval-request" ? [part.toolCallId] : []
|
|
35
|
+
part.type === "tool-approval-request" ? [part.toolCallId] : []
|
|
26
36
|
),
|
|
27
37
|
});
|
|
28
38
|
|
|
39
|
+
// The model answers each member with the same two calls.
|
|
40
|
+
const plan = (member: Member) =>
|
|
41
|
+
LanguageModel.generateText({
|
|
42
|
+
prompt: "Plan the launch.",
|
|
43
|
+
toolkit,
|
|
44
|
+
}).pipe(
|
|
45
|
+
actingFor(member),
|
|
46
|
+
Effect.map(outcome),
|
|
47
|
+
Effect.provide(
|
|
48
|
+
scripted([adding("for ada", "ada"), adding("for grace", "grace")])
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
|
|
29
52
|
describe("the toolkit", () => {
|
|
30
53
|
it.effect("runs each call as the member provided around it", () =>
|
|
31
54
|
Effect.gen(function* () {
|
|
@@ -33,9 +56,10 @@ describe("the toolkit", () => {
|
|
|
33
56
|
|
|
34
57
|
// A call by hand, with the JSON arguments a model sends. The member goes around the whole
|
|
35
58
|
// call, the collecting of its results included.
|
|
36
|
-
const add = handle("addTask", {
|
|
37
|
-
|
|
38
|
-
|
|
59
|
+
const add = handle("addTask", {
|
|
60
|
+
title: "Order lunch",
|
|
61
|
+
assignee: "guest",
|
|
62
|
+
}).pipe(Effect.flatMap(Stream.runCollect));
|
|
39
63
|
|
|
40
64
|
// A refusal is not raised: it is a result the model reads, as a declared error is.
|
|
41
65
|
expect(yield* add.pipe(actingFor(members.guest))).toMatchObject([
|
|
@@ -44,77 +68,82 @@ describe("the toolkit", () => {
|
|
|
44
68
|
expect(yield* add.pipe(actingFor(members.ada))).toMatchObject([
|
|
45
69
|
{ isFailure: false, result: { id: "1", addedBy: "ada" } },
|
|
46
70
|
]);
|
|
47
|
-
}).pipe(Effect.provide(tools))
|
|
71
|
+
}).pipe(Effect.provide(tools))
|
|
48
72
|
);
|
|
49
73
|
|
|
50
|
-
it.effect(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"grace"
|
|
69
|
-
|
|
70
|
-
}).pipe(Effect.provide(tools)),
|
|
74
|
+
it.effect(
|
|
75
|
+
"asks for approval before a model adds to a list that is not its caller's",
|
|
76
|
+
() =>
|
|
77
|
+
Effect.gen(function* () {
|
|
78
|
+
expect(yield* plan(members.ada)).toStrictEqual({
|
|
79
|
+
ran: ["for ada"],
|
|
80
|
+
waiting: ["for grace"],
|
|
81
|
+
});
|
|
82
|
+
expect(yield* plan(members.grace)).toStrictEqual({
|
|
83
|
+
ran: ["for grace"],
|
|
84
|
+
waiting: ["for ada"],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// A call that waits has not run.
|
|
88
|
+
const store = yield* TaskStore;
|
|
89
|
+
|
|
90
|
+
expect(
|
|
91
|
+
(yield* store.list(undefined)).map(({ addedBy }) => addedBy)
|
|
92
|
+
).toStrictEqual(["ada", "grace"]);
|
|
93
|
+
}).pipe(Effect.provide(tools))
|
|
71
94
|
);
|
|
72
95
|
|
|
73
|
-
it.effect(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
96
|
+
it.effect(
|
|
97
|
+
"authorizes an approved call too: approval lets no one do what they may not",
|
|
98
|
+
() =>
|
|
99
|
+
Effect.gen(function* () {
|
|
100
|
+
// The `Chat` keeps the request in its history, which the answer is matched against.
|
|
101
|
+
const chat = yield* Chat.empty;
|
|
102
|
+
|
|
103
|
+
const asked = yield* chat
|
|
104
|
+
.generateText({ prompt: "Add lunch to Ada's list.", toolkit })
|
|
105
|
+
.pipe(actingFor(members.guest));
|
|
106
|
+
|
|
107
|
+
expect(outcome(asked)).toStrictEqual({ ran: [], waiting: ["for ada"] });
|
|
108
|
+
|
|
109
|
+
// The approved call runs in the turn that carries the answer, as that turn's caller.
|
|
110
|
+
const answered = yield* chat
|
|
111
|
+
.generateText({
|
|
112
|
+
prompt: [
|
|
113
|
+
{
|
|
114
|
+
role: "tool",
|
|
115
|
+
content: asked.content.flatMap((part) =>
|
|
116
|
+
part.type === "tool-approval-request"
|
|
117
|
+
? [
|
|
118
|
+
{
|
|
119
|
+
type: "tool-approval-response",
|
|
120
|
+
approvalId: part.approvalId,
|
|
121
|
+
approved: true,
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
: []
|
|
125
|
+
),
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
toolkit,
|
|
129
|
+
})
|
|
130
|
+
.pipe(actingFor(members.guest));
|
|
131
|
+
|
|
132
|
+
expect(answered.toolResults).toMatchObject([
|
|
133
|
+
{
|
|
134
|
+
id: "for ada",
|
|
135
|
+
isFailure: true,
|
|
136
|
+
result: { message: "Requires tasks:write." },
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
const store = yield* TaskStore;
|
|
141
|
+
|
|
142
|
+
expect(yield* store.list(undefined)).toStrictEqual([]);
|
|
143
|
+
}).pipe(
|
|
144
|
+
Effect.provide(
|
|
145
|
+
scripted([adding("for ada", "ada")]).pipe(Layer.provideMerge(tools))
|
|
146
|
+
)
|
|
147
|
+
)
|
|
119
148
|
);
|
|
120
149
|
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# build-events
|
|
2
|
+
|
|
3
|
+
A CI service whose events are watched by its members: `build.finished`, read from a store that can be asked what finished since a cursor, and `deploy.requested`, published by the action that requests a deploy, with no store to ask. Each member sees only the pipelines they may. It serves both, with the actions that cause them, as an MCP endpoint, as HTTP routes and as Effect RPC, delivers them by webhook, keeping its subscriptions past a restart, and watches them in process, over each surface, and from a command line. A second service, the notifier, watches failed builds by webhook. Two actions answer more than one value: `startBuild` reports progress as it runs, and `tailLog` answers a stream of a build's log lines.
|
|
4
|
+
|
|
5
|
+
## Run it
|
|
6
|
+
|
|
7
|
+
From the repository root, after `vp install`:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
node --import tsx examples/build-events/src/server.ts
|
|
11
|
+
BUILDS_TOKEN=grace node --import tsx examples/build-events/src/cli.ts watch build.finished
|
|
12
|
+
BUILDS_TOKEN=grace PUBLIC_URL=https://<a tunnel to port 3001> node --import tsx examples/build-events/src/notifier.ts
|
|
13
|
+
vp test examples/build-events
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Layout
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
src/
|
|
20
|
+
├── identity/
|
|
21
|
+
│ ├── Identity.ts contract: CurrentMember and the Login descriptor, with its principal and schema
|
|
22
|
+
│ ├── members.ts the demo members
|
|
23
|
+
│ └── verifier.ts Login's verifier: a token is a member's name
|
|
24
|
+
├── builds/
|
|
25
|
+
│ ├── Builds.ts contract: the build schema, finishBuild, startBuild, tailLog and build.finished
|
|
26
|
+
│ ├── store.ts BuildStore: finished builds read by cursor, its epoch, and its Layer
|
|
27
|
+
│ ├── handlers.ts Action.implement: finishBuild, and startBuild reporting progress, each notifying build.finished
|
|
28
|
+
│ ├── log.ts Action.implement: tailLog, answering a stream of log lines
|
|
29
|
+
│ └── events.ts Event.implement: build.finished, a check source
|
|
30
|
+
├── deploys/
|
|
31
|
+
│ ├── Deploys.ts contract: the deploy schema, requestDeploy and deploy.requested
|
|
32
|
+
│ ├── approvers.ts Approvers: who stands by to approve deploys, and its Layer
|
|
33
|
+
│ ├── handlers.ts Action.implement: requestDeploy, publishing deploy.requested
|
|
34
|
+
│ └── events.ts Event.implement: deploy.requested, a log source with a subscription hook
|
|
35
|
+
├── Api.ts the HTTP binding: the actions' routes and the events' routes
|
|
36
|
+
├── Rpc.ts the RPC binding: the actions' rpcs and the events' stream rpcs
|
|
37
|
+
├── routes.ts the MCP endpoint, the HTTP routes and the rpcs, from one list of implementations, and webhook delivery
|
|
38
|
+
├── server.ts entrypoint: the stores, the events announced, a body limit, webhook delivery's client, resolver and store, and Node's HTTP server
|
|
39
|
+
├── commands.ts ActionCli.remote: a command per action, and `watch` per event type
|
|
40
|
+
├── cli.ts entrypoint: the commands, calling the server over HTTP
|
|
41
|
+
├── notifier/
|
|
42
|
+
│ ├── Notices.ts the failed builds the notifier was told of, and its Layer
|
|
43
|
+
│ └── routes.ts the notifier: its receiver, and its watch by webhook reading from it
|
|
44
|
+
└── notifier.ts entrypoint of the notifier, a second service
|
|
45
|
+
test/
|
|
46
|
+
├── support/ as: the caller of a call or subscription; inMemory: what the routes need, in memory
|
|
47
|
+
├── builds/ watching build.finished in process, a progress report and a log's stream
|
|
48
|
+
├── deploys/ watching deploy.requested in process
|
|
49
|
+
├── routes.test.ts watching both over MCP, streamed and polled, in memory
|
|
50
|
+
├── http.test.ts watching both over HTTP, as SSE and as JSON pages, in memory
|
|
51
|
+
├── rpc.test.ts watching over Effect RPC, in memory
|
|
52
|
+
├── webhooks.test.ts subscribing a callback, and the signed POSTs it receives, in memory
|
|
53
|
+
├── notifier.test.ts the notifier told of failed builds by webhook, both services in memory
|
|
54
|
+
└── commands.test.ts `watch build.finished` printing lines of JSON, in memory
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Where to look
|
|
58
|
+
|
|
59
|
+
| To see | Read |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| An event type beside the actions of its domain | `src/builds/Builds.ts` |
|
|
62
|
+
| A check source, its cursor the store's own | `src/builds/events.ts` |
|
|
63
|
+
| A cursor that tells a restart apart, as `truncated` | `src/builds/store.ts` |
|
|
64
|
+
| An action reporting progress as it runs | `src/builds/handlers.ts` |
|
|
65
|
+
| An action answering a stream | `src/builds/log.ts` |
|
|
66
|
+
| What a member may see, checked in the data access | `src/builds/store.ts` |
|
|
67
|
+
| A log source, and each subscriber's `match` | `src/deploys/events.ts` |
|
|
68
|
+
| Publishing to a log source from an action | `src/deploys/handlers.ts` |
|
|
69
|
+
| Waking a check source's subscribers after a write | `src/builds/handlers.ts` |
|
|
70
|
+
| Something set up upstream while a subscription lasts | `src/deploys/events.ts` |
|
|
71
|
+
| Watching events in process, with an action causing them | `test/builds/events.test.ts` |
|
|
72
|
+
| Serving events over MCP, and what it needs of `Login` | `src/routes.ts` |
|
|
73
|
+
| Watching over MCP, by stream and by poll | `test/routes.test.ts` |
|
|
74
|
+
| Binding events to HTTP routes beside the actions | `src/Api.ts` |
|
|
75
|
+
| Watching over HTTP, by SSE and by JSON pages | `test/http.test.ts` |
|
|
76
|
+
| Serving events as stream rpcs, and their serialization | `src/routes.ts` |
|
|
77
|
+
| Watching over RPC | `test/rpc.test.ts` |
|
|
78
|
+
| Watching from a command line, as lines of JSON | `src/commands.ts` |
|
|
79
|
+
| Delivering events by webhook, and what it needs | `src/routes.ts`, `src/server.ts` |
|
|
80
|
+
| Keeping webhook subscriptions past a restart | `src/server.ts`, `src/identity/Identity.ts` |
|
|
81
|
+
| Subscribing a callback, and checking each signature | `test/webhooks.test.ts` |
|
|
82
|
+
| Watching by webhook from a second service | `src/notifier/routes.ts` |
|
|
83
|
+
| Two services in memory, each calling the other | `test/notifier.test.ts` |
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BuildFinished,
|
|
5
|
+
FinishBuild,
|
|
6
|
+
PipelineStatus,
|
|
7
|
+
StartBuild,
|
|
8
|
+
TailLog,
|
|
9
|
+
} from "./builds/Builds.js";
|
|
10
|
+
import { DeployRequested, RequestDeploy } from "./deploys/Deploys.js";
|
|
11
|
+
import { Login } from "./identity/Identity.js";
|
|
12
|
+
|
|
13
|
+
// The HTTP binding: each action at `POST /api/<name>`, and each event at
|
|
14
|
+
// `POST /api/events/<name>`, which answers a page of JSON, or, asked for `text/event-stream`,
|
|
15
|
+
// holds a stream open. It imports no server code, so a client imports it with the contracts.
|
|
16
|
+
export const Http = ActionHttp.make(
|
|
17
|
+
[
|
|
18
|
+
FinishBuild,
|
|
19
|
+
StartBuild,
|
|
20
|
+
TailLog,
|
|
21
|
+
BuildFinished,
|
|
22
|
+
PipelineStatus,
|
|
23
|
+
RequestDeploy,
|
|
24
|
+
DeployRequested,
|
|
25
|
+
],
|
|
26
|
+
{ authentication: Login }
|
|
27
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BuildFinished,
|
|
5
|
+
FinishBuild,
|
|
6
|
+
PipelineStatus,
|
|
7
|
+
StartBuild,
|
|
8
|
+
TailLog,
|
|
9
|
+
} from "./builds/Builds.js";
|
|
10
|
+
import { DeployRequested, RequestDeploy } from "./deploys/Deploys.js";
|
|
11
|
+
import { Login } from "./identity/Identity.js";
|
|
12
|
+
|
|
13
|
+
// The RPC binding: each action an rpc named after it, and each event a stream rpc named after
|
|
14
|
+
// it, which pushes each occurrence as the server reads it. Plain data, as the HTTP binding is.
|
|
15
|
+
export const Rpc = ActionRpc.make(
|
|
16
|
+
[
|
|
17
|
+
FinishBuild,
|
|
18
|
+
StartBuild,
|
|
19
|
+
TailLog,
|
|
20
|
+
BuildFinished,
|
|
21
|
+
PipelineStatus,
|
|
22
|
+
RequestDeploy,
|
|
23
|
+
DeployRequested,
|
|
24
|
+
],
|
|
25
|
+
{ authentication: Login }
|
|
26
|
+
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import * as Event from "@gjermundgaraba/effect-actions/Event";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
import { CurrentMember } from "../identity/Identity.js";
|
|
6
|
+
|
|
7
|
+
const Pipeline = Schema.String.annotate({
|
|
8
|
+
description: "A pipeline's name, such as api.",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const Status = Schema.Literals(["passed", "failed"]).annotate({
|
|
12
|
+
description: "How the build ended.",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const Build = Schema.Struct({
|
|
16
|
+
id: Schema.String,
|
|
17
|
+
pipeline: Pipeline,
|
|
18
|
+
status: Status,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type Build = typeof Build.Type;
|
|
22
|
+
|
|
23
|
+
// DEMO ONLY: a CI runner reports finished builds; here a member does, so the example can
|
|
24
|
+
// cause the event it watches.
|
|
25
|
+
export const FinishBuild = Action.make("finishBuild", {
|
|
26
|
+
description: "Record that a build of a pipeline finished.",
|
|
27
|
+
input: { pipeline: Pipeline, status: Status },
|
|
28
|
+
success: Build,
|
|
29
|
+
readOnly: false,
|
|
30
|
+
caller: CurrentMember,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// A build run here, which reports each step as it starts, before it answers how the build ended.
|
|
34
|
+
// A caller reads the steps only if it asks, with `onProgress`; each surface carries them its
|
|
35
|
+
// own way. They are a snapshot to show: a caller falling behind misses some, never the answer.
|
|
36
|
+
// What a caller must see every time is the answer, or `build.finished`, which this build causes.
|
|
37
|
+
export const StartBuild = Action.make("startBuild", {
|
|
38
|
+
description:
|
|
39
|
+
"Run a build of a pipeline, reporting each step, and record how it ended.",
|
|
40
|
+
input: { pipeline: Pipeline },
|
|
41
|
+
success: Build,
|
|
42
|
+
progress: {
|
|
43
|
+
step: Schema.String.annotate({
|
|
44
|
+
description: "The step that started, such as test.",
|
|
45
|
+
}),
|
|
46
|
+
done: Schema.Finite.annotate({
|
|
47
|
+
description: "How many steps finished before it.",
|
|
48
|
+
}),
|
|
49
|
+
total: Schema.Finite.annotate({
|
|
50
|
+
description: "How many steps the build has.",
|
|
51
|
+
}),
|
|
52
|
+
},
|
|
53
|
+
readOnly: false,
|
|
54
|
+
caller: CurrentMember,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// A build this member may not see is not found, as one that never ran: its id tells nothing.
|
|
58
|
+
export class BuildNotFound extends Schema.TaggedError<
|
|
59
|
+
BuildNotFound,
|
|
60
|
+
{ readonly brand: unique symbol }
|
|
61
|
+
>()("BuildNotFound", { buildId: Schema.String }, { httpApiStatus: 404 }) {}
|
|
62
|
+
|
|
63
|
+
// An action answering a stream: its handler returns a `Stream` of lines, and every client's
|
|
64
|
+
// call returns one, each line arriving as the server sends it. HTTP, RPC and a command carry a
|
|
65
|
+
// stream; an MCP tool answers one result, the lines collected once the stream ends, so the MCP
|
|
66
|
+
// endpoint in `routes.ts` leaves it out rather than put a whole log in a model's context.
|
|
67
|
+
export const TailLog = Action.make("tailLog", {
|
|
68
|
+
description: "Stream a build's log, line by line.",
|
|
69
|
+
input: {
|
|
70
|
+
buildId: Schema.String.annotate({
|
|
71
|
+
description: "The build's id, such as build-1.",
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
success: Action.Stream({ line: Schema.String }),
|
|
75
|
+
error: BuildNotFound,
|
|
76
|
+
readOnly: true,
|
|
77
|
+
caller: CurrentMember,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// An event type: what a subscription takes, what each occurrence carries, and who may
|
|
81
|
+
// subscribe. Like an action's contract, it imports no server code.
|
|
82
|
+
export const BuildFinished = Event.make("build.finished", {
|
|
83
|
+
description: "A build of a pipeline you may see finished.",
|
|
84
|
+
input: {
|
|
85
|
+
pipeline: Schema.optional(Pipeline).annotate({
|
|
86
|
+
description: "Only this pipeline's builds.",
|
|
87
|
+
}),
|
|
88
|
+
status: Schema.optional(Status).annotate({
|
|
89
|
+
description: "Only builds that ended so.",
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
payload: Build,
|
|
93
|
+
caller: CurrentMember,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// An event whose occurrence is the whole value: how a pipeline's last build ended, `null` before
|
|
97
|
+
// its first. A subscriber is sent it as it is, then again each time a build of it finishes.
|
|
98
|
+
export const PipelineStatus = Event.make("pipeline.status", {
|
|
99
|
+
description:
|
|
100
|
+
"How a pipeline's last build ended: first as it is, then each time one finishes.",
|
|
101
|
+
input: { pipeline: Pipeline },
|
|
102
|
+
payload: {
|
|
103
|
+
status: Schema.NullOr(Status).annotate({
|
|
104
|
+
description: "How it ended; null before any did.",
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
caller: CurrentMember,
|
|
108
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as Event from "@gjermundgaraba/effect-actions/Event";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import { CurrentMember } from "../identity/Identity.js";
|
|
5
|
+
import { BuildFinished } from "./Builds.js";
|
|
6
|
+
import { BuildStore } from "./store.js";
|
|
7
|
+
|
|
8
|
+
// A check source: one function answers "what finished since this cursor?" for every
|
|
9
|
+
// subscriber, whether it polls or holds a stream open, on every surface. The builder runs once,
|
|
10
|
+
// at startup; the check runs per subscriber, as that subscriber.
|
|
11
|
+
export const buildEvents = Event.implement(
|
|
12
|
+
BuildFinished,
|
|
13
|
+
Effect.gen(function* () {
|
|
14
|
+
const store = yield* BuildStore;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
check: ({ pipeline, status }, cursor) =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const member = yield* CurrentMember;
|
|
20
|
+
|
|
21
|
+
// A first subscription asks with `null`: the store answers the current position, so
|
|
22
|
+
// the subscriber starts from now.
|
|
23
|
+
const page = yield* store.finishedSince(
|
|
24
|
+
member.pipelines,
|
|
25
|
+
cursor,
|
|
26
|
+
100
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
cursor: page.cursor,
|
|
31
|
+
hasMore: page.hasMore,
|
|
32
|
+
// A cursor the store cannot resume from: the subscriber is sent a gap first.
|
|
33
|
+
truncated: page.truncated,
|
|
34
|
+
events: page.builds
|
|
35
|
+
.filter(
|
|
36
|
+
(build) => pipeline === undefined || build.pipeline === pipeline
|
|
37
|
+
)
|
|
38
|
+
.filter(
|
|
39
|
+
(build) => status === undefined || build.status === status
|
|
40
|
+
)
|
|
41
|
+
// The store's own id, within its epoch: a subscriber that sees a build twice,
|
|
42
|
+
// after a reconnect, drops the second, and never drops a build of a restarted store
|
|
43
|
+
// as one it saw before.
|
|
44
|
+
.map((build) => ({
|
|
45
|
+
eventId: `${page.epoch}/${build.id}`,
|
|
46
|
+
data: build,
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}),
|
|
52
|
+
// Every member may watch; which builds each sees is the store's check, by pipeline.
|
|
53
|
+
{ authorize: Event.allowAll }
|
|
54
|
+
);
|