@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
package/dist/http/client.d.mts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { Codec } from "../contract/codec.mjs";
|
|
1
2
|
import { BuiltIns } from "../contract/errors.mjs";
|
|
3
|
+
import { Any, Received, SubscriptionError, WatchOptions } from "../Event.mjs";
|
|
4
|
+
import "../contract/implementation.mjs";
|
|
2
5
|
import { Call } from "../contract/call.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
+
import { Answer, StreamOf } from "../contract/stream.mjs";
|
|
7
|
+
import "../contract/receiving.mjs";
|
|
8
|
+
import { WatchArgs, WatchRequirement } from "../contract/watch.mjs";
|
|
9
|
+
import { Any as Any$1, Errors } from "../Action.mjs";
|
|
10
|
+
import { Any as Any$2, VerifierError } from "../authentication/provider.mjs";
|
|
11
|
+
import { sentOf } from "./events.mjs";
|
|
12
|
+
import { Effect, Schema, Stream } from "effect";
|
|
6
13
|
import { HttpClient, HttpClientError } from "effect/http";
|
|
7
14
|
import { HttpApi, HttpApiClient } from "effect/http-api";
|
|
8
15
|
/**
|
|
@@ -31,30 +38,48 @@ export interface Options<E = HttpClientError.HttpClientError, R = never> {
|
|
|
31
38
|
readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient.With<E, R>) | undefined;
|
|
32
39
|
}
|
|
33
40
|
type BindingError = Errors[number];
|
|
41
|
+
type SseFailure = Extract<Stream.Error<HttpApiClient.SuccessType<ReturnType<typeof sentOf<Any>>>>, {
|
|
42
|
+
readonly _tag: "SseError" | "Retry";
|
|
43
|
+
}>;
|
|
44
|
+
type SseError = Extract<SseFailure, {
|
|
45
|
+
readonly _tag: "SseError";
|
|
46
|
+
}>;
|
|
34
47
|
/**
|
|
35
48
|
* What one call of `A` fails with: a declared error value (the action's own, one `E` of the
|
|
36
49
|
* binding's, or a built-in `InvalidInput`, `Unauthenticated` or `Forbidden`), a native
|
|
37
50
|
* `HttpClientError` for a failed request or an undeclared answer, or a `SchemaError` when
|
|
38
|
-
* the success does not decode.
|
|
51
|
+
* the success does not decode. A call reading an event stream, of an action answering a stream or
|
|
52
|
+
* declaring `progress`, also fails with Effect's `Sse.SseError` or `Sse.Retry`.
|
|
39
53
|
*/
|
|
40
|
-
export type MethodError<A extends Any, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | HttpClientError.HttpClientError | Schema.SchemaError;
|
|
41
|
-
type Method<A extends Any, E extends BindingError, CE, CR> = Call<A,
|
|
54
|
+
export type MethodError<A extends Any$1, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | HttpClientError.HttpClientError | Schema.SchemaError | (A["progress"] extends undefined ? A["success"] extends StreamOf<Codec> ? SseFailure : never : SseFailure);
|
|
55
|
+
type Method<A extends Any$1, E extends BindingError, CE, CR> = Call<A, Answer<A["success"], MethodError<A, E> | CE, CR>>;
|
|
42
56
|
/**
|
|
43
57
|
* Any HTTP binding, with its actions erased: what `layer` and `client` read. The API is a native
|
|
44
58
|
* constraint here because native groups are invariant in their endpoints; `actions` carries the
|
|
45
59
|
* types.
|
|
46
60
|
*/
|
|
47
61
|
export interface AnyHttp {
|
|
48
|
-
readonly authentication?: Any$
|
|
49
|
-
readonly actions:
|
|
50
|
-
readonly
|
|
62
|
+
readonly authentication?: Any$2 | undefined;
|
|
63
|
+
readonly actions: readonly Any$1[];
|
|
64
|
+
readonly events: readonly Any[];
|
|
65
|
+
readonly error: readonly BindingError[];
|
|
51
66
|
readonly prefix: `/${string}`;
|
|
52
67
|
readonly api: HttpApi.Constraint;
|
|
53
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* What a watch of `W` over the binding `H` may fail with: what its route's first answer declares,
|
|
71
|
+
* a refusal ending it later, an `Event.SubscriptionError` where it is not served, a native
|
|
72
|
+
* `HttpClientError` that no reconnect outlasts, or a stream or an occurrence that does not decode.
|
|
73
|
+
*/
|
|
74
|
+
export type WatchError<W extends Any, H extends AnyHttp> = SubscriptionError | BuiltIns["Type"] | H["error"][number]["Type"] | VerifierError<W, H["authentication"]>["Type"] | HttpClientError.HttpClientError | Schema.SchemaError | SseError;
|
|
75
|
+
type Watching<H extends AnyHttp, E, R> = [H["events"][number]] extends [never] ? unknown : {
|
|
76
|
+
readonly watch: <W extends H["events"][number], SE = never, const O extends WatchOptions<unknown> = {}>(event: W, ...args: WatchArgs<W, SE, O>) => Stream.Stream<Received<W>, WatchError<W, H> | E | SE, R | WatchRequirement<O>>;
|
|
77
|
+
};
|
|
54
78
|
/**
|
|
55
79
|
* Every action of the binding `H`, as `client.<action>(input)`: a protected one may also fail
|
|
56
|
-
* with what its descriptor's verifier declares.
|
|
57
|
-
*
|
|
80
|
+
* with what its descriptor's verifier declares. Where `H` lists event types, `watch(event, input,
|
|
81
|
+
* options)` subscribes to one. Each call and watch also fails with `E` and requires `R`, what its
|
|
82
|
+
* `transformClient` adds.
|
|
58
83
|
*/
|
|
59
|
-
export type Client<H extends AnyHttp, E = never, R = never> = { readonly [A in H["actions"][number] as A["name"]]: Method<A, H["error"][number] | VerifierError<A, H["authentication"]>, E, R>; }
|
|
84
|
+
export type Client<H extends AnyHttp, E = never, R = never> = { readonly [A in H["actions"][number] as A["name"]]: Method<A, H["error"][number] | VerifierError<A, H["authentication"]>, E, R>; } & Watching<H, E, R>;
|
|
60
85
|
export {};
|
package/dist/http/client.mjs
CHANGED
|
@@ -1,18 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { elementOf } from "../contract/stream.mjs";
|
|
2
|
+
import { checked, checkedStream, sendingValue, watchInput } from "../contract/call.mjs";
|
|
3
|
+
import { watching } from "../contract/watch.mjs";
|
|
4
|
+
import { readElements, readWithProgress } from "./streamed.mjs";
|
|
5
|
+
import { transportOf } from "./watch.mjs";
|
|
6
|
+
import { Effect, Stream } from "effect";
|
|
7
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
4
8
|
import { HttpApiClient } from "effect/http-api";
|
|
5
9
|
// src/http/client.ts
|
|
6
|
-
const
|
|
10
|
+
const methodOf = (native, name, what) => {
|
|
11
|
+
const method = native[name];
|
|
12
|
+
if (method === void 0) throw new Error(`No client method for ${name} (use the binding \`ActionHttp.make\` returned, whose group holds this ${what})`);
|
|
13
|
+
return method;
|
|
14
|
+
};
|
|
15
|
+
const acceptingStreams = HttpClient.mapRequest(HttpClientRequest.setHeader("accept", "application/json, text/event-stream"));
|
|
16
|
+
const nativeOf = (http, httpClient, baseUrl) => HttpApiClient.makeWith(http.api, {
|
|
7
17
|
baseUrl,
|
|
8
18
|
httpClient
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
});
|
|
20
|
+
const actionMethodOf = (action, native) => {
|
|
21
|
+
const method = methodOf(native, action.name, "action");
|
|
22
|
+
if (elementOf(action.success) !== void 0) return checkedStream(action, sendingValue, (payload, onProgress) => Stream.unwrap(Effect.map(method({ payload }), (answer) => readElements(answer, onProgress))));
|
|
23
|
+
if (action.progress === void 0) return checked(action, sendingValue, (payload) => method({ payload }));
|
|
24
|
+
return checked(action, sendingValue, (payload, onProgress) => Effect.flatMap(method({ payload }), (answer) => readWithProgress(answer, onProgress)));
|
|
25
|
+
};
|
|
26
|
+
/** The `Senders` of `http` over `httpClient`: its native clients, built once. */
|
|
27
|
+
const sendersWith = (http, httpClient, baseUrl) => Effect.gen(function* () {
|
|
28
|
+
const native = yield* nativeOf(http, acceptingStreams(httpClient), baseUrl);
|
|
29
|
+
const plain = yield* nativeOf(http, httpClient, baseUrl);
|
|
30
|
+
const polls = plain;
|
|
31
|
+
const streams = native;
|
|
32
|
+
const subscribes = plain;
|
|
33
|
+
const unsubscribes = plain;
|
|
34
|
+
const webhookOf = (name) => {
|
|
35
|
+
const subscribe = subscribes[`${name}/subscribe`];
|
|
36
|
+
const unsubscribe = unsubscribes[`${name}/unsubscribe`];
|
|
37
|
+
return subscribe === void 0 || unsubscribe === void 0 ? void 0 : {
|
|
38
|
+
subscribe,
|
|
39
|
+
unsubscribe
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
return {
|
|
43
|
+
method: (action) => actionMethodOf(action, native),
|
|
44
|
+
transport: (event, input) => transportOf(event, methodOf(polls, event.name, "event"), methodOf(streams, event.name, "event"), webhookOf(event.name), input)
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
const methodsWith = (http, httpClient, baseUrl) => Effect.map(sendersWith(http, httpClient, baseUrl), ({ method, transport }) => {
|
|
48
|
+
const actions = Object.fromEntries(http.actions.map((action) => [action.name, method(action)]));
|
|
49
|
+
if (http.events.length === 0) return actions;
|
|
50
|
+
const watch = (event, input, options) => Stream.unwrap(Effect.map(watchInput(event, input), ({ value }) => watching(transport(event, value), options)));
|
|
51
|
+
return {
|
|
52
|
+
...actions,
|
|
53
|
+
watch
|
|
54
|
+
};
|
|
16
55
|
});
|
|
17
56
|
const methods = (http, { baseUrl, transformClient } = {}) => Effect.flatMap(HttpClient.HttpClient, (native) => transformClient === void 0 ? methodsWith(http, native, baseUrl) : methodsWith(http, transformClient(native), baseUrl));
|
|
18
|
-
export { methods };
|
|
57
|
+
export { methods, sendersWith };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import "../contract/codec.mjs";
|
|
2
|
+
import { Forbidden, Unauthenticated } from "../contract/errors.mjs";
|
|
3
|
+
import { OccurrenceSchema } from "../contract/occurrence.mjs";
|
|
4
|
+
import "../contract/pages.mjs";
|
|
5
|
+
import "../contract/feed.mjs";
|
|
6
|
+
import { Any } from "../Event.mjs";
|
|
7
|
+
import "../contract/subscribe.mjs";
|
|
8
|
+
import "../Action.mjs";
|
|
9
|
+
import "../authentication/provider.mjs";
|
|
10
|
+
import "../contract/push.mjs";
|
|
11
|
+
import "../contract/webhooks.mjs";
|
|
12
|
+
import { Effect, Option, Schema, Stream } from "effect";
|
|
13
|
+
import { HttpApiEndpoint, HttpApiSchema } from "effect/http-api";
|
|
14
|
+
/**
|
|
15
|
+
* One poll's answer: the occurrences since the cursor, and when to ask again, sent as
|
|
16
|
+
* `nextPollMs`, its milliseconds.
|
|
17
|
+
*/
|
|
18
|
+
export declare const pageOf: <E extends Any>(event: E) => Schema.encodeKeys<Schema.Struct<{
|
|
19
|
+
readonly events: Schema.$Array<OccurrenceSchema<E>>;
|
|
20
|
+
readonly cursor: Schema.NullOr<Schema.String>;
|
|
21
|
+
readonly truncated: Schema.Boolean;
|
|
22
|
+
readonly hasMore: Schema.Boolean;
|
|
23
|
+
readonly nextPoll: Schema.DurationFromMillis;
|
|
24
|
+
}>, {
|
|
25
|
+
readonly nextPoll: "nextPollMs";
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* What a subscription's event stream sends, each as one SSE `message`: a `Checked` with the
|
|
29
|
+
* position it starts from, or a `Gap` where that was no longer served, then each occurrence, a
|
|
30
|
+
* `Gap` where some were missed, a `Checked` every 30 seconds, and a `Failed` where reading
|
|
31
|
+
* failed and resumes. A later refusal ends it, as Effect's stream failure event.
|
|
32
|
+
*/
|
|
33
|
+
export declare const sentOf: <E extends Any>(event: E) => HttpApiSchema.StreamSse<HttpApiSchema.SseEventFromData<Schema.toCodecJson<Schema.Union<readonly [OccurrenceSchema<E>, Schema.TaggedStruct<"Gap", {
|
|
34
|
+
readonly cursor: Schema.NullOr<Schema.String>;
|
|
35
|
+
}>, Schema.TaggedStruct<"Checked", {
|
|
36
|
+
readonly cursor: Schema.NullOr<Schema.String>;
|
|
37
|
+
}>, Schema.TaggedStruct<"Failed", {}>]>>>, Schema.Union<readonly [typeof Unauthenticated, typeof Forbidden]>, {
|
|
38
|
+
readonly _tag: "Gap";
|
|
39
|
+
readonly cursor: string | null;
|
|
40
|
+
} | {
|
|
41
|
+
readonly _tag: "Checked";
|
|
42
|
+
readonly cursor: string | null;
|
|
43
|
+
} | {
|
|
44
|
+
readonly _tag: "Failed";
|
|
45
|
+
} | Schema.Struct.View<{
|
|
46
|
+
readonly _tag: Schema.tag<"Occurrence">;
|
|
47
|
+
eventId: Schema.String;
|
|
48
|
+
name: Schema.Literal<E["name"]>;
|
|
49
|
+
timestamp: Schema.DateTimeUtc;
|
|
50
|
+
data: E["payload"];
|
|
51
|
+
cursor: Schema.NullOr<Schema.String>;
|
|
52
|
+
}, "Type", E["payload"] extends {
|
|
53
|
+
readonly "~type.optionality": "optional";
|
|
54
|
+
} ? "data" : never, E["payload"] extends {
|
|
55
|
+
readonly "~type.mutability": "mutable";
|
|
56
|
+
} ? "data" : never>>;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { principalOf } from "../contract/caller.mjs";
|
|
2
|
+
import { Refusal } from "../contract/errors.mjs";
|
|
3
|
+
import { declared } from "../contract/status.mjs";
|
|
4
|
+
import { assertDistinctTags } from "../contract/declared.mjs";
|
|
5
|
+
import { OccurrenceOf } from "../contract/occurrence.mjs";
|
|
6
|
+
import { SubscriptionError } from "../Event.mjs";
|
|
7
|
+
import { webhookSubscriber } from "../contract/webhooks.mjs";
|
|
8
|
+
import { PushedOf, SubscriptionOf, queuedPush } from "../contract/push.mjs";
|
|
9
|
+
import { Subscribed, WebhookCancellationOf, WebhookSubscriptionOf } from "../contract/subscribe.mjs";
|
|
10
|
+
import { Effect, Schema, Stream } from "effect";
|
|
11
|
+
import { HttpServerRequest } from "effect/http";
|
|
12
|
+
import { HttpApiEndpoint, HttpApiSchema, OpenApi } from "effect/http-api";
|
|
13
|
+
// src/http/events.ts
|
|
14
|
+
/**
|
|
15
|
+
* One poll's answer: the occurrences since the cursor, and when to ask again, sent as
|
|
16
|
+
* `nextPollMs`, its milliseconds.
|
|
17
|
+
*/
|
|
18
|
+
const pageOf = (event) => Schema.Struct({
|
|
19
|
+
events: Schema.Array(OccurrenceOf(event)),
|
|
20
|
+
cursor: Schema.NullOr(Schema.String),
|
|
21
|
+
truncated: Schema.Boolean,
|
|
22
|
+
hasMore: Schema.Boolean,
|
|
23
|
+
nextPoll: Schema.DurationFromMillis
|
|
24
|
+
}).pipe(Schema.encodeKeys({ nextPoll: "nextPollMs" }));
|
|
25
|
+
/**
|
|
26
|
+
* What a subscription's event stream sends, each as one SSE `message`: a `Checked` with the
|
|
27
|
+
* position it starts from, or a `Gap` where that was no longer served, then each occurrence, a
|
|
28
|
+
* `Gap` where some were missed, a `Checked` every 30 seconds, and a `Failed` where reading
|
|
29
|
+
* failed and resumes. A later refusal ends it, as Effect's stream failure event.
|
|
30
|
+
*/
|
|
31
|
+
const sentOf = (event) => HttpApiSchema.StreamSse({
|
|
32
|
+
data: Schema.toCodecJson(PushedOf(event)),
|
|
33
|
+
error: Refusal
|
|
34
|
+
});
|
|
35
|
+
/** What a served subscription's event stream sends: each element's JSON, as `sentOf` encodes it. */
|
|
36
|
+
const servedSent = HttpApiSchema.StreamSse({
|
|
37
|
+
data: Schema.Json,
|
|
38
|
+
error: Refusal
|
|
39
|
+
});
|
|
40
|
+
/**
|
|
41
|
+
* The route of one event type at `path`, its poll answered as JSON and its stream as SSE, as
|
|
42
|
+
* `sent` declares it.
|
|
43
|
+
*/
|
|
44
|
+
const eventEndpointOf = (event, path, errors, sent = sentOf(event)) => HttpApiEndpoint.post(event.name, path, {
|
|
45
|
+
payload: SubscriptionOf(event).pipe(HttpApiSchema.asJson()),
|
|
46
|
+
success: [pageOf(event), sent],
|
|
47
|
+
error: errors.map(declared)
|
|
48
|
+
}).annotate(OpenApi.Description, event.description);
|
|
49
|
+
/**
|
|
50
|
+
* The webhook routes of one protected event type at `path`: `/subscribe`, which starts or
|
|
51
|
+
* refreshes a subscription and answers its grant, and `/unsubscribe`, which ends it.
|
|
52
|
+
*/
|
|
53
|
+
const webhookEndpointsOf = (event, path, errors) => {
|
|
54
|
+
const withWebhooks = [...errors, SubscriptionError];
|
|
55
|
+
assertDistinctTags(`ActionHttp binding of event "${event.name}"'s webhook routes`, withWebhooks);
|
|
56
|
+
const error = withWebhooks.map(declared);
|
|
57
|
+
return [HttpApiEndpoint.post(`${event.name}/subscribe`, `${path}/subscribe`, {
|
|
58
|
+
payload: WebhookSubscriptionOf(event).pipe(HttpApiSchema.asJson()),
|
|
59
|
+
success: Subscribed,
|
|
60
|
+
error
|
|
61
|
+
}).annotate(OpenApi.Description, `Deliver ${event.name} to a webhook, or refresh its grant.`), HttpApiEndpoint.post(`${event.name}/unsubscribe`, `${path}/unsubscribe`, {
|
|
62
|
+
payload: WebhookCancellationOf(event).pipe(HttpApiSchema.asJson()),
|
|
63
|
+
success: HttpApiSchema.NoContent,
|
|
64
|
+
error
|
|
65
|
+
}).annotate(OpenApi.Description, `Stop delivering ${event.name} to a webhook.`)];
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* The handlers of `feed`'s webhook routes, through `webhooks` where the host provided
|
|
69
|
+
* `Webhook.layer`; without it, both fail with `Unsupported`.
|
|
70
|
+
*/
|
|
71
|
+
const webhookAnswers = (feed, authentication, webhooks) => {
|
|
72
|
+
const served = webhookSubscriber(feed, authentication, webhooks);
|
|
73
|
+
return {
|
|
74
|
+
subscribe: ({ payload }) => Effect.flatMap(served, ({ webhooks: service, ...subscriber }) => service.subscribe({
|
|
75
|
+
...payload,
|
|
76
|
+
...subscriber
|
|
77
|
+
})),
|
|
78
|
+
unsubscribe: ({ payload }) => Effect.flatMap(served, ({ webhooks: service, feed: subscribed, principal }) => service.unsubscribe({
|
|
79
|
+
...payload,
|
|
80
|
+
feed: subscribed,
|
|
81
|
+
principal
|
|
82
|
+
}))
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Answers a subscription's request to `feed`: a page, or, where it accepts `text/event-stream`,
|
|
87
|
+
* the subscription's stream, each element its JSON. Both admit the subscriber first, so a refusal
|
|
88
|
+
* is the route's error; `authentication`'s principal names a poll's subscriber, for its
|
|
89
|
+
* `subscription` hook.
|
|
90
|
+
*/
|
|
91
|
+
const answering = (feed, authentication) => {
|
|
92
|
+
const encode = Schema.encodeEffect(Schema.toCodecJson(PushedOf(feed.event)));
|
|
93
|
+
return ({ payload: { input, cursor } }) => Effect.gen(function* () {
|
|
94
|
+
if (((yield* HttpServerRequest.HttpServerRequest).headers["accept"] ?? "").includes("text/event-stream")) {
|
|
95
|
+
const open = yield* feed.stream(input);
|
|
96
|
+
return Stream.fromQueue(yield* queuedPush(open, cursor, encode));
|
|
97
|
+
}
|
|
98
|
+
const subscriber = yield* principalOf(feed.event, authentication);
|
|
99
|
+
return yield* feed.poll(input, cursor, { subscriber });
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
export { answering, eventEndpointOf, pageOf, sentOf, servedSent, webhookAnswers, webhookEndpointsOf };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { EventImplementation } from "../contract/feed.mjs";
|
|
2
|
+
import { Any } from "../Event.mjs";
|
|
3
|
+
import { Implementation } from "../contract/implementation.mjs";
|
|
4
|
+
import { Any as Any$1 } from "../Action.mjs";
|
|
5
|
+
import { AnyHttp } from "./client.mjs";
|
|
6
|
+
import { HttpClient } from "effect/http";
|
|
7
|
+
type Contract<H extends AnyHttp> = H["actions"][number] | H["events"][number];
|
|
8
|
+
/** A call's or a subscription's decoded input, beside its contract, one of `C`. */
|
|
9
|
+
/** What the contracts `C` require per request, by name: nothing. */
|
|
10
|
+
type Nothing<C extends Any$1 | Any> = { readonly [K in C["name"]]: never; };
|
|
11
|
+
/**
|
|
12
|
+
* What `placed` returns: an implementation of the binding's actions where it has some, and one of
|
|
13
|
+
* its event types where it has some, which require nothing.
|
|
14
|
+
*/
|
|
15
|
+
export type Placed<H extends AnyHttp> = ReadonlyArray<Implementation<H["actions"][number], Nothing<H["actions"][number]>, never, never> | EventImplementation<H["events"][number], Nothing<H["events"][number]>, never, never>>;
|
|
16
|
+
/**
|
|
17
|
+
* The actions and event types of `binding` served where another server serves them, as a
|
|
18
|
+
* surface's implementations: each call and subscription sent on over `binding` with the client
|
|
19
|
+
* `client` answers for its decoded input and its contract, one of the binding's, carrying the
|
|
20
|
+
* caller's headers, and its answer, its errors and its occurrences passed back as they are. The
|
|
21
|
+
* surface serving them authenticates a protected one as its own and authorizes every caller; the
|
|
22
|
+
* server reached authenticates and authorizes again.
|
|
23
|
+
*
|
|
24
|
+
* **Gotchas**
|
|
25
|
+
*
|
|
26
|
+
* - The caller's credential is verified twice, by the surface serving these and by the server reached, so both are given the verifier: a credential expiring between the two is refused by the second.
|
|
27
|
+
* - The server reached builds its own verifier: one caching signing keys in memory fetches them in each of its instances, a Durable Object's again after each eviction.
|
|
28
|
+
* - Every header of the caller's request is sent on but its connection's, its body's, what it accepts and an MCP request's routing headers (`mcp-*`): one that must not reach the server reached is removed per contract by the client `client` returns, `HttpClient.mapRequest(HttpClientRequest.removeHeader("cookie"))`, which runs after them.
|
|
29
|
+
* - A header `client`'s client sets replaces the caller's of that name, as it is set after them: a service's own `authorization` is sent in place of the caller's.
|
|
30
|
+
* - A call with no HTTP request, as through `Action.client` in process or an MCP stdio surface, sends no header on: a protected one reaches the server reached with no credential, which refuses it with `Unauthenticated`.
|
|
31
|
+
* - An error the server reached declares beyond the action's own and the built-in ones, its binding's `error` or its verifier's, is a defect where it is passed back, answered as one.
|
|
32
|
+
* - A server reached that does not serve one of them, its route a 404, is a defect naming it, with the fix: a declared error whose status is 404 is passed back as it is.
|
|
33
|
+
* - A refusal of a subscription by the server reached comes after the surface admitted it, so it ends the subscription's stream where the surface's own would have answered its 401 or 403.
|
|
34
|
+
*/
|
|
35
|
+
export declare const placed: <const H extends AnyHttp>(binding: H, client: (input: Contract<H>["input"]["Type"], contract: Contract<H>) => HttpClient.HttpClient) => Placed<H>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Implementation, memoized } from "../contract/implementation.mjs";
|
|
2
|
+
import { EventImplementation } from "../contract/feed.mjs";
|
|
3
|
+
import { SubscriptionError, allowAll } from "../Event.mjs";
|
|
4
|
+
import { isNotFound } from "./watch.mjs";
|
|
5
|
+
import { sendersWith } from "./client.mjs";
|
|
6
|
+
import { allowAll as allowAll$1 } from "../Action.mjs";
|
|
7
|
+
import { Context, Effect, Option, Predicate, Schema, Stream } from "effect";
|
|
8
|
+
import { Headers, HttpClient, HttpClientRequest, HttpServerRequest } from "effect/http";
|
|
9
|
+
// src/http/placed.ts
|
|
10
|
+
const ownedByTheRequest = (name) => [
|
|
11
|
+
"host",
|
|
12
|
+
"connection",
|
|
13
|
+
"keep-alive",
|
|
14
|
+
"te",
|
|
15
|
+
"trailer",
|
|
16
|
+
"transfer-encoding",
|
|
17
|
+
"upgrade"
|
|
18
|
+
].includes(name) || [
|
|
19
|
+
"content-",
|
|
20
|
+
"accept",
|
|
21
|
+
"proxy-",
|
|
22
|
+
"sec-websocket-",
|
|
23
|
+
"mcp-"
|
|
24
|
+
].some((family) => name.startsWith(family));
|
|
25
|
+
const forwarded = Effect.map(Effect.serviceOption(HttpServerRequest.HttpServerRequest), Option.match({
|
|
26
|
+
onNone: () => Headers.empty,
|
|
27
|
+
onSome: ({ headers }) => Headers.fromInput(Object.entries(headers).filter(([name]) => !ownedByTheRequest(name)))
|
|
28
|
+
}));
|
|
29
|
+
var Sending = class extends Context.Service()("effect-actions/ActionHttp/placed/Sending") {};
|
|
30
|
+
const dispatching = HttpClient.makeWith((request) => Effect.flatMap(Effect.all([request, Effect.service(Sending)]), ([sent, client]) => client.execute(sent)), (request) => Effect.succeed(request));
|
|
31
|
+
const isSubscriptionError = Schema.is(SubscriptionError);
|
|
32
|
+
const isNotServed = Predicate.compose(isSubscriptionError, (error) => Predicate.isTagged(error.reason, "NotServed"));
|
|
33
|
+
const notServedThere = (name) => /* @__PURE__ */ new Error(`ActionHttp.placed: the server its client reaches does not serve ${name} (serve its implementation in that server's ActionHttp.layer of this binding)`);
|
|
34
|
+
/**
|
|
35
|
+
* The actions and event types of `binding` served where another server serves them, as a
|
|
36
|
+
* surface's implementations: each call and subscription sent on over `binding` with the client
|
|
37
|
+
* `client` answers for its decoded input and its contract, one of the binding's, carrying the
|
|
38
|
+
* caller's headers, and its answer, its errors and its occurrences passed back as they are. The
|
|
39
|
+
* surface serving them authenticates a protected one as its own and authorizes every caller; the
|
|
40
|
+
* server reached authenticates and authorizes again.
|
|
41
|
+
*
|
|
42
|
+
* **Gotchas**
|
|
43
|
+
*
|
|
44
|
+
* - The caller's credential is verified twice, by the surface serving these and by the server reached, so both are given the verifier: a credential expiring between the two is refused by the second.
|
|
45
|
+
* - The server reached builds its own verifier: one caching signing keys in memory fetches them in each of its instances, a Durable Object's again after each eviction.
|
|
46
|
+
* - Every header of the caller's request is sent on but its connection's, its body's, what it accepts and an MCP request's routing headers (`mcp-*`): one that must not reach the server reached is removed per contract by the client `client` returns, `HttpClient.mapRequest(HttpClientRequest.removeHeader("cookie"))`, which runs after them.
|
|
47
|
+
* - A header `client`'s client sets replaces the caller's of that name, as it is set after them: a service's own `authorization` is sent in place of the caller's.
|
|
48
|
+
* - A call with no HTTP request, as through `Action.client` in process or an MCP stdio surface, sends no header on: a protected one reaches the server reached with no credential, which refuses it with `Unauthenticated`.
|
|
49
|
+
* - An error the server reached declares beyond the action's own and the built-in ones, its binding's `error` or its verifier's, is a defect where it is passed back, answered as one.
|
|
50
|
+
* - A server reached that does not serve one of them, its route a 404, is a defect naming it, with the fix: a declared error whose status is 404 is passed back as it is.
|
|
51
|
+
* - A refusal of a subscription by the server reached comes after the surface admitted it, so it ends the subscription's stream where the surface's own would have answered its 401 or 403.
|
|
52
|
+
*/
|
|
53
|
+
const placed = (binding, client) => {
|
|
54
|
+
const route = client;
|
|
55
|
+
const sendingTo = (input, contract) => HttpClient.mapRequestInputEffect(route(input, contract), (request) => Effect.map(forwarded, (headers) => HttpClientRequest.setHeaders(request, headers)));
|
|
56
|
+
const senders = sendersWith(binding, dispatching, void 0);
|
|
57
|
+
const handlerOf = ({ method }, action) => {
|
|
58
|
+
const send = method(action);
|
|
59
|
+
return (input, reporting) => {
|
|
60
|
+
const sent = send(input, ...reporting === void 0 ? [] : [{ onProgress: reporting.progress }]);
|
|
61
|
+
const sending = sendingTo(input, action);
|
|
62
|
+
return Stream.isStream(sent) ? sent.pipe(Stream.provideService(Sending, sending), Stream.catchIf(isNotFound, () => Stream.die(notServedThere(action.name)))) : sent.pipe(Effect.provideService(Sending, sending), Effect.catchIf(isNotFound, () => Effect.die(notServedThere(action.name))));
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const runOf = ({ transport }, event) => ({
|
|
66
|
+
poll: (input, cursor) => transport(event, input).poll(cursor).pipe(Effect.provideService(Sending, sendingTo(input, event)), Effect.catchIf(isNotServed, () => Effect.die(notServedThere(event.name)))),
|
|
67
|
+
stream: (input, cursor) => transport(event, input).stream(cursor).pipe(Stream.provideService(Sending, sendingTo(input, event)), Stream.catchIf(isNotServed, () => Stream.die(notServedThere(event.name))))
|
|
68
|
+
});
|
|
69
|
+
return [...binding.actions.length === 0 ? [] : [new Implementation(binding.actions, memoized(Effect.map(senders, (built) => binding.actions.map((action) => [action, handlerOf(built, action)]))), memoized(Effect.succeed(allowAll$1)))], ...binding.events.length === 0 ? [] : [new EventImplementation(binding.events, binding.events.map((event) => [event, memoized(Effect.map(senders, (built) => runOf(built, event)))]), memoized(Effect.succeed(allowAll)))]];
|
|
70
|
+
};
|
|
71
|
+
export { placed };
|
|
@@ -4,5 +4,5 @@ import { HttpApiMiddleware } from "effect/http-api";
|
|
|
4
4
|
// src/http/schema-errors.ts
|
|
5
5
|
const responseEncodingKinds = /* @__PURE__ */ new Set(["Body", "ResponseHeaders"]);
|
|
6
6
|
var SchemaErrors = class extends HttpApiMiddleware.Service()("effect-actions/http/SchemaErrors", { error: [InvalidInput] }) {};
|
|
7
|
-
const schemaErrors =
|
|
7
|
+
const schemaErrors = HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrors, (failure) => Effect.fail(responseEncodingKinds.has(failure.kind) ? failure : InvalidInput.fromSchemaError(failure.cause)));
|
|
8
8
|
export { SchemaErrors, schemaErrors };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Codec } from "../contract/codec.mjs";
|
|
2
|
+
import "../contract/progress.mjs";
|
|
3
|
+
import { ErasedValue } from "../contract/implementation.mjs";
|
|
4
|
+
import { StreamOf } from "../contract/stream.mjs";
|
|
5
|
+
import { Any } from "../Action.mjs";
|
|
6
|
+
import { Effect, Schema, Stream } from "effect";
|
|
7
|
+
import { HttpApiSchema } from "effect/http-api";
|
|
8
|
+
/** One event a route streams: a progress value, an element, or the answer it ends with. */
|
|
9
|
+
interface Sent {
|
|
10
|
+
readonly event: "progress" | "element" | "final";
|
|
11
|
+
readonly data: ErasedValue;
|
|
12
|
+
}
|
|
13
|
+
declare const eventOf: <const N extends Sent["event"], D extends Codec>(event: N, data: D) => Schema.Struct<{
|
|
14
|
+
readonly id: Schema.optional<Schema.String>;
|
|
15
|
+
readonly event: Schema.Literal<N>;
|
|
16
|
+
readonly data: Schema.fromJsonString<Schema.toCodecJson<D>>;
|
|
17
|
+
}>;
|
|
18
|
+
type EventOf<N extends Sent["event"], D extends Codec> = ReturnType<typeof eventOf<N, D>>;
|
|
19
|
+
/** The success of the endpoint of action `A` declaring errors `E`, as `ActionHttp` builds it. */
|
|
20
|
+
export type SuccessOf<A extends Any, E extends Schema.Top> = A["success"] extends StreamOf<infer Element> ? HttpApiSchema.StreamSse<Schema.Union<A["progress"] extends Codec ? readonly [EventOf<"element", Element>, EventOf<"progress", A["progress"]>] : readonly [EventOf<"element", Element>]>, E> : A["progress"] extends Codec ? Schema.toCodecJson<A["success"]> | HttpApiSchema.StreamSse<Schema.Union<readonly [EventOf<"progress", A["progress"]>, EventOf<"final", A["success"]>]>, E> : Schema.toCodecJson<A["success"]>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Progressed, answerFrom, elementsFrom } from "../contract/progress.mjs";
|
|
2
|
+
import { elementsOf } from "../contract/implementation.mjs";
|
|
3
|
+
import { concealed } from "../contract/reported.mjs";
|
|
4
|
+
import { Cause, Effect, ErrorReporter, Queue, Schema, Stream } from "effect";
|
|
5
|
+
import { HttpServerRequest } from "effect/http";
|
|
6
|
+
import { HttpApiSchema } from "effect/http-api";
|
|
7
|
+
// src/http/streamed.ts
|
|
8
|
+
const eventOf = (event, data) => Schema.Struct({
|
|
9
|
+
id: Schema.optional(Schema.String),
|
|
10
|
+
event: Schema.Literal(event),
|
|
11
|
+
data: Schema.fromJsonString(Schema.toCodecJson(data))
|
|
12
|
+
});
|
|
13
|
+
/** What a route of an action declaring `progress` answers without its stream: `success`. */
|
|
14
|
+
const answeredWithProgress = (success) => HttpApiSchema.isNoContent(success.ast) ? HttpApiSchema.NoContent : success;
|
|
15
|
+
/**
|
|
16
|
+
* What a route of an action declaring `progress` answers a request accepting
|
|
17
|
+
* `text/event-stream`: a `progress` event for each value its handler reports, then a `final`
|
|
18
|
+
* event with its answer. A declared failure after the first event ends the stream as Effect's
|
|
19
|
+
* stream failure event, and a defect as one saying `Internal server error`.
|
|
20
|
+
*/
|
|
21
|
+
const progressStreamOf = (progress, success, errors) => HttpApiSchema.StreamSse({
|
|
22
|
+
events: Schema.Union([eventOf("progress", progress), eventOf("final", success)]),
|
|
23
|
+
error: Schema.Union(errors)
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* What a route of an action answering a stream answers: an `element` event for each element,
|
|
27
|
+
* beside a `progress` event for each value its handler reports where it declares `progress`. A
|
|
28
|
+
* declared failure after the first event ends the stream as Effect's stream failure event, and
|
|
29
|
+
* a defect as one saying `Internal server error`.
|
|
30
|
+
*/
|
|
31
|
+
const elementStreamOf = (element, progress, errors) => HttpApiSchema.StreamSse({
|
|
32
|
+
events: Schema.Union(progress === void 0 ? [eventOf("element", element)] : [eventOf("element", element), eventOf("progress", progress)]),
|
|
33
|
+
error: Schema.Union(errors)
|
|
34
|
+
});
|
|
35
|
+
const streamed = (answer) => Effect.gen(function* () {
|
|
36
|
+
const sent = yield* Queue.bounded(1);
|
|
37
|
+
let streaming = false;
|
|
38
|
+
const send = (event) => Effect.andThen(Queue.offer(sent, event), Effect.sync(() => {
|
|
39
|
+
streaming = true;
|
|
40
|
+
}));
|
|
41
|
+
const reporting = { progress: (data) => send({
|
|
42
|
+
event: "progress",
|
|
43
|
+
data
|
|
44
|
+
}) };
|
|
45
|
+
const ended = (cause) => {
|
|
46
|
+
if (Cause.hasDies(cause)) return concealed(cause);
|
|
47
|
+
return streaming ? Effect.andThen(ErrorReporter.report(cause), Effect.failCause(cause)) : Effect.failCause(cause);
|
|
48
|
+
};
|
|
49
|
+
yield* answer(reporting, send).pipe(Effect.catchCause(ended), Queue.into(sent), Effect.forkScoped({ startImmediately: true }));
|
|
50
|
+
const first = yield* Effect.catchIf(Queue.take(sent), Cause.isDone, () => Effect.undefined);
|
|
51
|
+
return first === void 0 ? Stream.empty : Stream.concat(Stream.make(first), Stream.fromQueue(sent));
|
|
52
|
+
});
|
|
53
|
+
/** A check of `schema` failing as a defect, inside the call's boundary. */
|
|
54
|
+
const checkedBy = (schema) => {
|
|
55
|
+
const encode = Schema.encodeUnknownEffect(Schema.toCodecJson(schema));
|
|
56
|
+
return (data) => Effect.orDie(encode(data));
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The route handler of an action declaring `progress`: its answer streamed with its progress
|
|
60
|
+
* where the request's `Accept` names `text/event-stream`, its answer alone otherwise.
|
|
61
|
+
*/
|
|
62
|
+
const answeringWithProgress = (success, run) => {
|
|
63
|
+
const check = checkedBy(success);
|
|
64
|
+
return (payload) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => (request.headers["accept"] ?? "").includes("text/event-stream") ? streamed((reporting, send) => run(payload, reporting).pipe(Effect.tap(check), Effect.flatMap((data) => send({
|
|
65
|
+
event: "final",
|
|
66
|
+
data
|
|
67
|
+
})))) : run(payload));
|
|
68
|
+
};
|
|
69
|
+
/** The route handler of an action answering a stream: its elements and its progress, streamed. */
|
|
70
|
+
const answeringElements = (element, run) => {
|
|
71
|
+
const check = checkedBy(element);
|
|
72
|
+
return (payload) => streamed((reporting, send) => Stream.runForEach(elementsOf(run(payload, reporting)), (data) => Effect.andThen(check(data), send({
|
|
73
|
+
event: "element",
|
|
74
|
+
data
|
|
75
|
+
}))));
|
|
76
|
+
};
|
|
77
|
+
const progressedOf = ({ event, data }) => {
|
|
78
|
+
if (event === "progress") return Progressed.Progress({ value: data });
|
|
79
|
+
return event === "final" ? Progressed.Final({ value: data }) : Progressed.Element({ value: data });
|
|
80
|
+
};
|
|
81
|
+
const answeredWithout = /* @__PURE__ */ new Error("A route of an action declaring progress or answering a stream answered without its event stream");
|
|
82
|
+
/** The events of a route's answer, as one stream of `Progressed`. */
|
|
83
|
+
const sentFrom = (answer) => Stream.isStream(answer) ? Stream.map(answer, progressedOf) : Stream.die(answeredWithout);
|
|
84
|
+
/**
|
|
85
|
+
* A client's answer from a route of an action declaring `progress`: each `progress` event handed
|
|
86
|
+
* to `onProgress`, where the call gave one, then the `final` event's answer.
|
|
87
|
+
*/
|
|
88
|
+
const readWithProgress = (answer, onProgress) => answerFrom(sentFrom(answer), onProgress);
|
|
89
|
+
/**
|
|
90
|
+
* A client's elements from a route of an action answering a stream: each `element` event's
|
|
91
|
+
* data, each `progress` event's handed to `onProgress`.
|
|
92
|
+
*/
|
|
93
|
+
const readElements = (answer, onProgress) => elementsFrom(sentFrom(answer), onProgress);
|
|
94
|
+
export { answeredWithProgress, answeringElements, answeringWithProgress, elementStreamOf, progressStreamOf, readElements, readWithProgress };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { fedOf } from "../contract/pages.mjs";
|
|
2
|
+
import { NotServed, SubscriptionError } from "../Event.mjs";
|
|
3
|
+
import { Receiving, webhookFed } from "../contract/receiving.mjs";
|
|
4
|
+
import { answeredStatus, failingWhenUnanswered, isTransient, receivedFrom } from "../contract/transient.mjs";
|
|
5
|
+
import { Effect, Predicate, Stream } from "effect";
|
|
6
|
+
import { HttpClientError } from "effect/http";
|
|
7
|
+
// src/http/watch.ts
|
|
8
|
+
/** Whether `error` is an HTTP client's failure on a 404 answer. */
|
|
9
|
+
const isNotFound = Predicate.compose(HttpClientError.isHttpClientError, (error) => answeredStatus(error) === 404);
|
|
10
|
+
/**
|
|
11
|
+
* The transport of one subscription to `event` over its route, `input` its decoded input: each
|
|
12
|
+
* page from `poll`, the stream from `stream`, and a webhook subscription from `webhook`'s routes
|
|
13
|
+
* where the binding has them, a 404 failing as `NotServed`.
|
|
14
|
+
*/
|
|
15
|
+
const transportOf = (event, poll, stream, webhook, input) => {
|
|
16
|
+
const notServed = Effect.fail(SubscriptionError.of(event.name, new NotServed()));
|
|
17
|
+
const answered = (request) => request.pipe(failingWhenUnanswered, Effect.catchIf(isNotFound, () => notServed));
|
|
18
|
+
const webhooks = webhook === void 0 ? void 0 : {
|
|
19
|
+
receiver: Effect.service(Receiving),
|
|
20
|
+
subscribe: (request) => answered(webhook.subscribe({ payload: {
|
|
21
|
+
input,
|
|
22
|
+
...request
|
|
23
|
+
} })),
|
|
24
|
+
unsubscribe: (request) => answered(webhook.unsubscribe({ payload: {
|
|
25
|
+
input,
|
|
26
|
+
...request
|
|
27
|
+
} })),
|
|
28
|
+
read: webhookFed(event)
|
|
29
|
+
};
|
|
30
|
+
const read = (sent) => sent.pipe(Stream.catchIf(Predicate.isTagged("Retry"), () => Stream.empty), receivedFrom("HTTP", event.name));
|
|
31
|
+
return {
|
|
32
|
+
name: event.name,
|
|
33
|
+
poll: (cursor) => answered(poll({ payload: {
|
|
34
|
+
input,
|
|
35
|
+
cursor
|
|
36
|
+
} })),
|
|
37
|
+
stream: (cursor) => Stream.unwrap(answered(stream({ payload: {
|
|
38
|
+
input,
|
|
39
|
+
cursor
|
|
40
|
+
} })).pipe(Effect.map((answer) => Stream.isStream(answer) ? read(answer) : Stream.fromIterable(fedOf({
|
|
41
|
+
items: answer.events,
|
|
42
|
+
truncated: answer.truncated,
|
|
43
|
+
from: cursor,
|
|
44
|
+
to: answer.cursor ?? cursor
|
|
45
|
+
}))))),
|
|
46
|
+
webhook: webhooks,
|
|
47
|
+
transient: isTransient
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export { isNotFound, transportOf };
|