@gjermundgaraba/effect-actions 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -41
- package/dist/Action.d.mts +140 -48
- package/dist/Action.mjs +88 -44
- package/dist/ActionCli.d.mts +70 -35
- package/dist/ActionCli.mjs +53 -34
- package/dist/ActionHttp.d.mts +70 -38
- package/dist/ActionHttp.mjs +76 -25
- package/dist/ActionMcp.d.mts +56 -24
- package/dist/ActionMcp.mjs +104 -61
- package/dist/ActionRpc.d.mts +222 -57
- package/dist/ActionRpc.mjs +301 -49
- package/dist/ActionToolkit.d.mts +22 -7
- package/dist/ActionToolkit.mjs +37 -14
- package/dist/Authentication.d.mts +42 -12
- package/dist/Authentication.mjs +38 -32
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.d.mts +1 -3
- package/dist/Testing.mjs +2 -3
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -12
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -4
- package/dist/cli/commands.d.mts +9 -7
- package/dist/cli/commands.mjs +85 -35
- package/dist/contract/ast.mjs +5 -3
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -8
- package/dist/contract/call.mjs +49 -8
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +12 -3
- package/dist/contract/caller.mjs +23 -5
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +26 -11
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -5
- package/dist/contract/errors.mjs +2 -3
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hints.mjs +1 -2
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -51
- package/dist/contract/implementation.mjs +94 -60
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/media.d.mts +1 -3
- package/dist/contract/media.mjs +1 -2
- package/dist/contract/names.mjs +15 -6
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +15 -7
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -14
- package/dist/http/client.mjs +52 -14
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +2 -3
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -99
- package/dist/mcp/client.mjs +60 -134
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +40 -15
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +14 -7
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +4 -5
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +57 -61
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -53
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +25 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +19 -17
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "./pages.mjs";
|
|
2
|
+
import { Any, WatchOptions, WebhookDelivery } from "../Event.mjs";
|
|
3
|
+
import { Receiving } from "./receiving.mjs";
|
|
4
|
+
import "./subscribe.mjs";
|
|
5
|
+
import { Effect, Schema, Stream } from "effect";
|
|
6
|
+
/**
|
|
7
|
+
* The arguments of `watch(event, ...)` after the event: its input, which may be left out when
|
|
8
|
+
* `{}` is a valid encoded input, as for an action's call, and the watch's options `O`, whose
|
|
9
|
+
* `resume` store may fail with `SE`.
|
|
10
|
+
*/
|
|
11
|
+
export type WatchArgs<E extends Any, SE, O extends WatchOptions<unknown> = WatchOptions<SE>> = {} extends E["input"]["Encoded"] ? [input?: E["input"]["Type"], options?: O & WatchOptions<SE>] : [input: E["input"]["Type"], options?: O & WatchOptions<SE>];
|
|
12
|
+
/**
|
|
13
|
+
* What a watch given the options `O` requires beyond its client's: the receiver, where its `mode`
|
|
14
|
+
* may be a `Webhook.delivery`.
|
|
15
|
+
*/
|
|
16
|
+
export type WatchRequirement<O> = O extends {
|
|
17
|
+
readonly mode?: infer M;
|
|
18
|
+
} ? [Extract<M, WebhookDelivery>] extends [never] ? never : Receiving : never;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Checked, Gap } from "./occurrence.mjs";
|
|
2
|
+
import { paged } from "./pages.mjs";
|
|
3
|
+
import { SubscriptionError, Unsupported } from "../Event.mjs";
|
|
4
|
+
import { watchParameter } from "./receiving.mjs";
|
|
5
|
+
import { makeRecent } from "./recent.mjs";
|
|
6
|
+
import { CallbackUrl } from "./callback.mjs";
|
|
7
|
+
import { Cause, Clock, Duration, Effect, Predicate, Queue, Schedule, Schema, Stream } from "effect";
|
|
8
|
+
// src/contract/watch.ts
|
|
9
|
+
const reconnectDelay = Duration.seconds(1);
|
|
10
|
+
const maxReconnectDelay = Duration.seconds(30);
|
|
11
|
+
const afterFailures = (transient) => Schedule.min([Schedule.exponential(reconnectDelay), Schedule.spaced(maxReconnectDelay)]).pipe(Schedule.setInputType(), Schedule.while(({ input }) => transient(input)), Schedule.tap(({ input, duration }) => Effect.logWarning(`A watch reconnects in ${Duration.format(duration)} after a failure`, input)));
|
|
12
|
+
const isSubscriptionError = Schema.is(SubscriptionError);
|
|
13
|
+
/** Whether a watch's failure says the server serves its event by poll alone, refusing to push it. */
|
|
14
|
+
const pushRefused = Predicate.compose(isSubscriptionError, ({ reason }) => Predicate.isTagged(reason, "Unsupported") && reason.feature === "deliveryMode" && reason.value === "push");
|
|
15
|
+
const dedupeWindow = 1e3;
|
|
16
|
+
const capacity = 16;
|
|
17
|
+
const unsubscribeTimeout = Duration.seconds(5);
|
|
18
|
+
/**
|
|
19
|
+
* One webhook subscription from `cursor`, read from `received`, where its receiver puts each
|
|
20
|
+
* body: each grant, the subscribe's and each refresh's when half of it is left, is put there too,
|
|
21
|
+
* behind what was accepted before it, so its position is kept in order; a refresh's failure is
|
|
22
|
+
* put there as well, and ends the run where it is read.
|
|
23
|
+
*/
|
|
24
|
+
const delivered = (webhook, request, received, cursor) => {
|
|
25
|
+
let at = cursor;
|
|
26
|
+
const subscribed = Effect.suspend(() => webhook.subscribe({
|
|
27
|
+
...request,
|
|
28
|
+
cursor: at
|
|
29
|
+
})).pipe(Effect.tap((grant) => Queue.offer(received, Effect.succeed([grant.truncated ? Gap.make({ cursor: grant.cursor }) : Checked.make({ cursor: grant.cursor })]))));
|
|
30
|
+
const refreshed = (refreshBefore) => refreshBefore === null ? Effect.never : Effect.flatMap(Clock.currentTimeMillis, (now) => Effect.andThen(Effect.sleep(Duration.millis(Math.max(0, (Date.parse(refreshBefore) - now) / 2))), Effect.flatMap(subscribed, (grant) => refreshed(grant.refreshBefore))));
|
|
31
|
+
return Stream.unwrap(Effect.gen(function* () {
|
|
32
|
+
const grant = yield* subscribed;
|
|
33
|
+
yield* Effect.forkScoped(Effect.catchCause(refreshed(grant.refreshBefore), (cause) => Cause.hasInterruptsOnly(cause) ? Effect.void : Queue.offer(received, Effect.failCause(cause))));
|
|
34
|
+
return Stream.fromEffectRepeat(Effect.flatten(Queue.take(received))).pipe(Stream.flattenIterable, Stream.tap((fed) => Effect.sync(() => {
|
|
35
|
+
at = fed.cursor ?? at;
|
|
36
|
+
})));
|
|
37
|
+
}));
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The occurrences and gaps of one subscription, as every client's `watch` answers them. Each run
|
|
41
|
+
* starts from `options`: what one run saw, the cursor and the ids it deduplicates by, ends with
|
|
42
|
+
* it, and only `resume` carries a cursor to the next.
|
|
43
|
+
*/
|
|
44
|
+
const watching = (transport, { cursor: given, mode, resume } = {}) => {
|
|
45
|
+
const reconnecting = (open, cursor, polled) => {
|
|
46
|
+
const { transient } = transport;
|
|
47
|
+
let at = cursor;
|
|
48
|
+
const once = Stream.suspend(() => open(at)).pipe(Stream.tap((fed) => Effect.sync(() => {
|
|
49
|
+
at = fed.cursor ?? at;
|
|
50
|
+
})));
|
|
51
|
+
const reopened = transient === void 0 ? once : once.pipe(Stream.repeat(Schedule.spaced(reconnectDelay)), Stream.retry(afterFailures(transient)));
|
|
52
|
+
return polled === void 0 ? reopened : reopened.pipe(Stream.catchIf(pushRefused, () => Stream.suspend(() => polled(at))));
|
|
53
|
+
};
|
|
54
|
+
const polled = (cursor) => reconnecting((at) => paged(transport.poll, at), cursor);
|
|
55
|
+
const webhooked = ({ baseUrl, ttl }, cursor) => Stream.unwrap(Effect.gen(function* () {
|
|
56
|
+
const { webhook } = transport;
|
|
57
|
+
if (webhook === void 0) return yield* SubscriptionError.of(transport.name, new Unsupported({
|
|
58
|
+
feature: "deliveryMode",
|
|
59
|
+
value: "webhook"
|
|
60
|
+
}));
|
|
61
|
+
const base = yield* Schema.decodeEffect(CallbackUrl)(baseUrl);
|
|
62
|
+
const receiving = yield* webhook.receiver;
|
|
63
|
+
const received = yield* Queue.unbounded();
|
|
64
|
+
const { id, secret } = yield* receiving.register((body) => Queue.sizeUnsafe(received) < capacity && Queue.offerUnsafe(received, webhook.read(body)));
|
|
65
|
+
const url = new URL(`${base.pathname.replace(/\/$/u, "")}${receiving.path}`, base);
|
|
66
|
+
url.searchParams.set(watchParameter, id);
|
|
67
|
+
const request = {
|
|
68
|
+
delivery: {
|
|
69
|
+
url,
|
|
70
|
+
secret
|
|
71
|
+
},
|
|
72
|
+
ttlMs: ttl === void 0 ? void 0 : Duration.toMillis(Duration.fromInputUnsafe(ttl))
|
|
73
|
+
};
|
|
74
|
+
yield* Effect.addFinalizer(() => webhook.unsubscribe({ delivery: { url } }).pipe(Effect.timeout(unsubscribeTimeout), Effect.ignore));
|
|
75
|
+
return reconnecting((at) => delivered(webhook, request, received, at), cursor);
|
|
76
|
+
}));
|
|
77
|
+
return Stream.suspend(() => {
|
|
78
|
+
const seen = makeRecent(dedupeWindow);
|
|
79
|
+
let pending = null;
|
|
80
|
+
const keep = (cursor) => cursor === null || resume === void 0 ? Effect.void : resume.store.set(resume.key, cursor);
|
|
81
|
+
const fresh = (eventId) => {
|
|
82
|
+
if (seen.has(eventId)) return false;
|
|
83
|
+
seen.add(eventId);
|
|
84
|
+
return true;
|
|
85
|
+
};
|
|
86
|
+
const handled = Effect.fnUntraced(function* (fed) {
|
|
87
|
+
yield* keep(pending);
|
|
88
|
+
pending = null;
|
|
89
|
+
if (Predicate.isTagged(fed, "Checked")) {
|
|
90
|
+
yield* keep(fed.cursor);
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
if (Predicate.isTagged(fed, "Gap")) return [fed];
|
|
94
|
+
pending = fed.cursor;
|
|
95
|
+
return fresh(fed.eventId) ? [fed] : [];
|
|
96
|
+
});
|
|
97
|
+
const start = given !== void 0 || resume === void 0 ? Effect.succeed(given ?? null) : Effect.map(resume.store.get(resume.key), (kept) => kept ?? null);
|
|
98
|
+
return Stream.unwrap(Effect.map(start, (cursor) => {
|
|
99
|
+
if (Predicate.isTagged(mode, "WebhookDelivery")) return webhooked(mode, cursor);
|
|
100
|
+
if (mode === "poll") return polled(cursor);
|
|
101
|
+
return reconnecting(transport.stream, cursor, mode === "stream" ? void 0 : polled);
|
|
102
|
+
})).pipe(Stream.rechunk(1), Stream.mapEffect(handled), Stream.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Stream.failCause(cause) : Stream.concat(Stream.fromEffectDrain(keep(pending)), Stream.failCause(cause))), Stream.flattenIterable);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
export { watching };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Anyone, Named } from "./caller.mjs";
|
|
2
|
+
import { Feed } from "./feed.mjs";
|
|
3
|
+
import { Any } from "../Event.mjs";
|
|
4
|
+
import { AnyWebhookCancellation, AnyWebhookSubscription, Subscribed } from "./subscribe.mjs";
|
|
5
|
+
import { Context, Effect, Option } from "effect";
|
|
6
|
+
/** An event type a surface may offer by webhook: a protected one, its caller an identity. */
|
|
7
|
+
export type Protected = Any & {
|
|
8
|
+
readonly caller: Exclude<Any["caller"], typeof Anyone>;
|
|
9
|
+
};
|
|
10
|
+
/** A feed a surface offers by webhook: one of a protected event type. */
|
|
11
|
+
export interface WebhookFeed extends Feed {
|
|
12
|
+
readonly event: Protected;
|
|
13
|
+
}
|
|
14
|
+
/** Which webhook subscription a request names: the subscriber, the event, its input, the URL. */
|
|
15
|
+
export interface Subscription extends AnyWebhookCancellation {
|
|
16
|
+
readonly feed: WebhookFeed;
|
|
17
|
+
readonly principal: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A request to start or refresh a webhook subscription, as its surface decoded it, `keyed` the
|
|
21
|
+
* surface's authentication, whose `schema` keeps the subscriber's identity.
|
|
22
|
+
*/
|
|
23
|
+
export interface Subscribing extends AnyWebhookSubscription {
|
|
24
|
+
readonly feed: WebhookFeed;
|
|
25
|
+
readonly principal: string;
|
|
26
|
+
readonly keyed: Named;
|
|
27
|
+
}
|
|
28
|
+
declare const Webhooks_base: Context.ServiceClass<Webhooks, "effect-actions/Webhooks", {
|
|
29
|
+
readonly serve: (feeds: readonly WebhookFeed[], keyed: Named) => Effect.Effect<void, never, unknown>;
|
|
30
|
+
readonly subscribe: (subscribing: Subscribing) => Effect.Effect<Subscribed, unknown, unknown>;
|
|
31
|
+
readonly unsubscribe: (subscription: Subscription) => Effect.Effect<void, unknown, unknown>;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Where a surface's webhook subscriptions are held and delivered from: `Webhook.layer` provides
|
|
35
|
+
* it, and a surface offers webhook delivery only where it is. A surface `serve`s the feeds it
|
|
36
|
+
* offers by webhook as it builds, with its authentication, which restores their kept
|
|
37
|
+
* subscriptions, and dies where they cannot be kept. `subscribe` runs as the subscriber, whose
|
|
38
|
+
* context each delivery runs in; it fails as the feed's admission does, or with
|
|
39
|
+
* `Event.SubscriptionError`.
|
|
40
|
+
*/
|
|
41
|
+
export declare class Webhooks extends Webhooks_base {}
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Anyone, isNamed } from "./caller.mjs";
|
|
2
|
+
import { SubscriptionError, Unsupported } from "../Event.mjs";
|
|
3
|
+
import { Context, Effect, Option } from "effect";
|
|
4
|
+
// src/contract/webhooks.ts
|
|
5
|
+
/**
|
|
6
|
+
* Where a surface's webhook subscriptions are held and delivered from: `Webhook.layer` provides
|
|
7
|
+
* it, and a surface offers webhook delivery only where it is. A surface `serve`s the feeds it
|
|
8
|
+
* offers by webhook as it builds, with its authentication, which restores their kept
|
|
9
|
+
* subscriptions, and dies where they cannot be kept. `subscribe` runs as the subscriber, whose
|
|
10
|
+
* context each delivery runs in; it fails as the feed's admission does, or with
|
|
11
|
+
* `Event.SubscriptionError`.
|
|
12
|
+
*/
|
|
13
|
+
var Webhooks = class extends Context.Service()("effect-actions/Webhooks") {};
|
|
14
|
+
const isProtected = (event) => event.caller !== Anyone;
|
|
15
|
+
/** Whether a surface offers `event` by webhook: a protected event, its subscribers `keyed`. */
|
|
16
|
+
const offersWebhook = (event, keyed) => isProtected(event) && isNamed(keyed);
|
|
17
|
+
/**
|
|
18
|
+
* Serves the feeds among `feeds` a surface offers by webhook, its subscribers `keyed`, to the
|
|
19
|
+
* `Webhooks` it is given, as it builds: restoring their kept subscriptions, and dying where they
|
|
20
|
+
* cannot be kept. Answers the service, where given, and those feeds.
|
|
21
|
+
*/
|
|
22
|
+
const servedWebhooks = (feeds, keyed) => Effect.gen(function* () {
|
|
23
|
+
const webhooks = yield* Effect.serviceOption(Webhooks);
|
|
24
|
+
const offered = feeds.filter((feed) => offersWebhook(feed.event, keyed));
|
|
25
|
+
if (Option.isSome(webhooks) && isNamed(keyed)) yield* webhooks.value.serve(offered, keyed);
|
|
26
|
+
return {
|
|
27
|
+
webhooks,
|
|
28
|
+
offered
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* The webhook subscriber calling a subscribe or unsubscribe of `feed`'s event on a surface given
|
|
33
|
+
* `webhooks`, its subscribers `keyed`; `Unsupported` where the surface does not offer it by
|
|
34
|
+
* webhook.
|
|
35
|
+
*/
|
|
36
|
+
const webhookSubscriber = (feed, keyed, webhooks) => {
|
|
37
|
+
const { event } = feed;
|
|
38
|
+
if (Option.isNone(webhooks) || !isNamed(keyed) || !isProtected(event)) return Effect.fail(SubscriptionError.of(event.name, new Unsupported({
|
|
39
|
+
feature: "deliveryMode",
|
|
40
|
+
value: "webhook"
|
|
41
|
+
})));
|
|
42
|
+
const service = webhooks.value;
|
|
43
|
+
return Effect.map(Effect.service(event.caller), (identity) => ({
|
|
44
|
+
webhooks: service,
|
|
45
|
+
feed: {
|
|
46
|
+
...feed,
|
|
47
|
+
event
|
|
48
|
+
},
|
|
49
|
+
keyed,
|
|
50
|
+
principal: keyed.principal(identity)
|
|
51
|
+
}));
|
|
52
|
+
};
|
|
53
|
+
export { Webhooks, offersWebhook, servedWebhooks, webhookSubscriber };
|
package/dist/http/client.d.mts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
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
|
-
//#region src/http/client.d.ts
|
|
9
15
|
/**
|
|
10
16
|
* The native `HttpApiClient.make` options except `transformResponse`, which may change a call's
|
|
11
17
|
* success, failure or services the method types cannot follow: `baseUrl`, and `transformClient`,
|
|
@@ -32,31 +38,48 @@ export interface Options<E = HttpClientError.HttpClientError, R = never> {
|
|
|
32
38
|
readonly transformClient?: ((client: HttpClient.HttpClient) => HttpClient.HttpClient.With<E, R>) | undefined;
|
|
33
39
|
}
|
|
34
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
|
+
}>;
|
|
35
47
|
/**
|
|
36
48
|
* What one call of `A` fails with: a declared error value (the action's own, one `E` of the
|
|
37
49
|
* binding's, or a built-in `InvalidInput`, `Unauthenticated` or `Forbidden`), a native
|
|
38
50
|
* `HttpClientError` for a failed request or an undeclared answer, or a `SchemaError` when
|
|
39
|
-
* 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`.
|
|
40
53
|
*/
|
|
41
|
-
export type MethodError<A extends Any, E extends BindingError = never> = A["error"][number]["Type"] | E["Type"] | BuiltIns["Type"] | HttpClientError.HttpClientError | Schema.SchemaError;
|
|
42
|
-
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>>;
|
|
43
56
|
/**
|
|
44
57
|
* Any HTTP binding, with its actions erased: what `layer` and `client` read. The API is a native
|
|
45
58
|
* constraint here because native groups are invariant in their endpoints; `actions` carries the
|
|
46
59
|
* types.
|
|
47
60
|
*/
|
|
48
61
|
export interface AnyHttp {
|
|
49
|
-
readonly authentication?: Any$
|
|
50
|
-
readonly actions:
|
|
51
|
-
readonly
|
|
62
|
+
readonly authentication?: Any$2 | undefined;
|
|
63
|
+
readonly actions: readonly Any$1[];
|
|
64
|
+
readonly events: readonly Any[];
|
|
65
|
+
readonly error: readonly BindingError[];
|
|
52
66
|
readonly prefix: `/${string}`;
|
|
53
67
|
readonly api: HttpApi.Constraint;
|
|
54
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
|
+
};
|
|
55
78
|
/**
|
|
56
79
|
* Every action of the binding `H`, as `client.<action>(input)`: a protected one may also fail
|
|
57
|
-
* with what its descriptor's verifier declares.
|
|
58
|
-
*
|
|
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.
|
|
59
83
|
*/
|
|
60
|
-
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>; }
|
|
61
|
-
//#endregion
|
|
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>;
|
|
62
85
|
export {};
|
package/dist/http/client.mjs
CHANGED
|
@@ -1,19 +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
|
-
|
|
6
|
-
const
|
|
9
|
+
// src/http/client.ts
|
|
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
|
-
|
|
19
|
-
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 {};
|