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