@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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Forbidden } from "./errors.mjs";
|
|
2
|
+
import { Gap, untaggedOccurrenceOf } from "./occurrence.mjs";
|
|
3
|
+
import { reasonOf, refusalOf } from "./codes.mjs";
|
|
4
|
+
import { Context, Effect, Option, Schema, Struct } from "effect";
|
|
5
|
+
// src/contract/receiving.ts
|
|
6
|
+
/** The query parameter of a watch's delivery URL naming its place at the receiver. */
|
|
7
|
+
const watchParameter = "watch";
|
|
8
|
+
/**
|
|
9
|
+
* Where a watch by webhook receives its deliveries in this process: `Webhook.layerReceiver`
|
|
10
|
+
* provides it beside its route at `path`, and `register` keeps a watch's place for its scope,
|
|
11
|
+
* handing each verified body to `deliver`, which answers `false` while the watch has no room
|
|
12
|
+
* for it.
|
|
13
|
+
*/
|
|
14
|
+
var Receiving = class extends Context.Service()("effect-actions/Receiving") {};
|
|
15
|
+
const Terminated = Schema.Struct({ error: Schema.Struct({
|
|
16
|
+
code: Schema.optional(Schema.Int),
|
|
17
|
+
message: Schema.String,
|
|
18
|
+
data: Schema.optional(Schema.Json)
|
|
19
|
+
}) });
|
|
20
|
+
const decodeTerminated = Schema.decodeUnknownOption(Terminated);
|
|
21
|
+
const decodeGap = Schema.decodeUnknownEffect(Schema.toCodecJson(Gap.mapFields(Struct.omit(["_tag"]))));
|
|
22
|
+
const ended = new Forbidden({ message: "The subscription was ended." });
|
|
23
|
+
/**
|
|
24
|
+
* What a watch of `event` reads from one delivered body: its occurrence, or the gap an envelope
|
|
25
|
+
* reports; a `terminated` envelope fails with the reason its error's code names, its refusal for
|
|
26
|
+
* any other, an envelope of another type is skipped, and a body that does not decode fails with a
|
|
27
|
+
* `SchemaError`.
|
|
28
|
+
*/
|
|
29
|
+
const webhookFed = (event) => {
|
|
30
|
+
const occurrenceOf = untaggedOccurrenceOf(event);
|
|
31
|
+
return (body) => {
|
|
32
|
+
const { type } = body;
|
|
33
|
+
if (type === void 0) return Effect.map(occurrenceOf(body), (occurrence) => [occurrence]);
|
|
34
|
+
if (type === "gap") return Effect.map(decodeGap(body), (fields) => [Gap.make(fields)]);
|
|
35
|
+
if (type === "terminated") return Effect.fail(Option.match(decodeTerminated(body), {
|
|
36
|
+
onNone: () => ended,
|
|
37
|
+
onSome: ({ error }) => reasonOf(event.name, error) ?? refusalOf(error)
|
|
38
|
+
}));
|
|
39
|
+
return Effect.succeed([]);
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export { Receiving, watchParameter, webhookFed };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/contract/recent.ts
|
|
2
|
+
const makeRecent = (size) => {
|
|
3
|
+
const ids = /* @__PURE__ */ new Set();
|
|
4
|
+
return {
|
|
5
|
+
has: (id) => ids.has(id),
|
|
6
|
+
add: (id) => {
|
|
7
|
+
ids.add(id);
|
|
8
|
+
if (ids.size > size) ids.delete(ids.values().next().value ?? id);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export { makeRecent };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Effect, ErrorReporter } from "effect";
|
|
2
|
+
// src/contract/reported.ts
|
|
3
|
+
/** Logs `cause` and gives it to the request's `ErrorReporter`s, as a failed call's is. */
|
|
4
|
+
const reported = (cause) => Effect.andThen(Effect.logError(cause), ErrorReporter.report(cause));
|
|
5
|
+
var InternalError = class extends Error {
|
|
6
|
+
[ErrorReporter.ignore] = true;
|
|
7
|
+
constructor() {
|
|
8
|
+
super("Internal server error");
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* `cause` `reported`, then a defect that says no more than "Internal server error", reported no
|
|
13
|
+
* further: what a surface sends where it would otherwise encode a defect's details.
|
|
14
|
+
*/
|
|
15
|
+
const concealed = (cause) => Effect.andThen(reported(cause), Effect.die(new InternalError()));
|
|
16
|
+
export { concealed, reported };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Anyone, assertAuthentication } from "./caller.mjs";
|
|
2
|
+
import { assertDistinct, assertOnce, heldOnce } from "./names.mjs";
|
|
3
|
+
import { Implementation, assertHeld, markingOthers, notHeld, provideBuilders, select, servedBy, toList } from "./implementation.mjs";
|
|
4
|
+
import { eventsOf, feedBuilders, selectEvents } from "./feed.mjs";
|
|
5
|
+
import { selection } from "./listed.mjs";
|
|
6
|
+
// src/contract/serving.ts
|
|
7
|
+
/**
|
|
8
|
+
* What a surface without a binding serves of `served`: the actions and event types `actions`
|
|
9
|
+
* lists, each implementation holding those alone, or every one where it lists none.
|
|
10
|
+
*/
|
|
11
|
+
const selectFrom = (served, actions) => {
|
|
12
|
+
const listed = selection(actions);
|
|
13
|
+
return {
|
|
14
|
+
apps: select(toList(served), listed.actions),
|
|
15
|
+
events: selectEvents(eventsOf(served), listed.events)
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* The event implementations among `served` a binding of `bound` serves, each holding the served
|
|
20
|
+
* event types alone: those `listed` lists, or else every bound one, refusing implementations none
|
|
21
|
+
* of whose event types it binds, as `servedBy` refuses such action implementations.
|
|
22
|
+
*/
|
|
23
|
+
const eventsServedBy = (what, bound, served, listed) => {
|
|
24
|
+
const apps = eventsOf(served);
|
|
25
|
+
if (listed !== void 0) assertHeld(notHeld.byBinding, listed, bound);
|
|
26
|
+
const unbound = apps.filter((app) => !app.events.some((event) => bound.includes(event)));
|
|
27
|
+
if (listed === void 0 && unbound.length > 0 && unbound.length === apps.length) {
|
|
28
|
+
const names = new Set(bound.map(({ name }) => name));
|
|
29
|
+
throw new Error(`No event of these implementations is in this ${what}: ${markingOthers(unbound.flatMap((app) => app.events), names)} (pass an implementation of a bound event, or bind one of these)`);
|
|
30
|
+
}
|
|
31
|
+
const held = new Set(apps.flatMap((app) => app.events));
|
|
32
|
+
const selected = selectEvents(apps, listed ?? bound.filter((event) => held.has(event)));
|
|
33
|
+
assertOnce("served event", selected.flatMap((app) => app.events), heldOnce);
|
|
34
|
+
return selected;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* What `ActionHttp.layer` and `ActionRpc.layer` serve, with the checks they share: the actions
|
|
38
|
+
* and event types `options.actions` lists, or every one of the binding's the implementations
|
|
39
|
+
* hold; `authentication` is the binding's where a served contract is protected.
|
|
40
|
+
*/
|
|
41
|
+
const servingOf = (what, binding, served, options) => {
|
|
42
|
+
const listed = selection(options.actions);
|
|
43
|
+
const apps = servedBy(what, binding.actions, toList(served), listed.actions);
|
|
44
|
+
const actions = apps.flatMap((app) => app.actions);
|
|
45
|
+
const events = eventsServedBy(what, binding.events, served, listed.events);
|
|
46
|
+
const eventTypes = events.flatMap((app) => app.events);
|
|
47
|
+
assertAuthentication(actions, binding.authentication);
|
|
48
|
+
assertAuthentication(eventTypes, binding.authentication, "event");
|
|
49
|
+
assertDistinct("middleware", options.middleware ?? [], (key) => key.key, { fix: "list each middleware once" });
|
|
50
|
+
return {
|
|
51
|
+
apps,
|
|
52
|
+
actions,
|
|
53
|
+
events,
|
|
54
|
+
eventTypes,
|
|
55
|
+
authentication: [...actions, ...eventTypes].some((contract) => contract.caller !== Anyone) ? binding.authentication : void 0
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Provide the builders of what `servingOf` selects, the action implementations' and the event
|
|
60
|
+
* implementations', to a surface's layer.
|
|
61
|
+
*/
|
|
62
|
+
const provideServed = (served) => provideBuilders([...served.apps.map((app) => Implementation.layerOf(app)), ...feedBuilders(served.events)]);
|
|
63
|
+
export { provideServed, selectFrom, servingOf };
|
package/dist/contract/status.mjs
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { SchemaAST } from "effect";
|
|
2
2
|
// src/contract/status.ts
|
|
3
3
|
const httpApiStatusOf = SchemaAST.resolveAt("httpApiStatus");
|
|
4
|
+
const transientStatuses = /* @__PURE__ */ new Set([
|
|
5
|
+
408,
|
|
6
|
+
429,
|
|
7
|
+
500,
|
|
8
|
+
502,
|
|
9
|
+
503,
|
|
10
|
+
504
|
|
11
|
+
]);
|
|
12
|
+
/** Whether a server answering `status` may answer otherwise once it recovers. */
|
|
13
|
+
const isTransientStatus = (status) => transientStatuses.has(status);
|
|
4
14
|
const statusThroughSuspensions = (ast) => httpApiStatusOf(ast) ?? (SchemaAST.isSuspend(ast) ? statusThroughSuspensions(ast.thunk()) : void 0);
|
|
15
|
+
/** The status an endpoint answers `error` with: its own, through suspensions, 422 without one. */
|
|
16
|
+
const declaredStatus = (error) => statusThroughSuspensions(error.ast) ?? 422;
|
|
5
17
|
/** One error schema, from `errorSchemas`, as an endpoint declares it: with its status, 422 without one. */
|
|
6
|
-
const declared = (error) => {
|
|
7
|
-
|
|
8
|
-
return status === void 0 ? error.annotate({ httpApiStatus: 422 }) : httpApiStatusOf(error.ast) === void 0 ? error.annotate({ httpApiStatus: status }) : error;
|
|
9
|
-
};
|
|
10
|
-
export { declared, httpApiStatusOf };
|
|
18
|
+
const declared = (error) => httpApiStatusOf(error.ast) === void 0 ? error.annotate({ httpApiStatus: declaredStatus(error) }) : error;
|
|
19
|
+
export { declared, declaredStatus, httpApiStatusOf, isTransientStatus };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Codec, CodecOf, Fields } from "./codec.mjs";
|
|
2
|
+
import { Effect, Schema, Stream } from "effect";
|
|
3
|
+
declare const TypeId = "~effect-actions/Stream";
|
|
4
|
+
/** The success of an action answering a stream: a schema of `Stream`s, holding its element's. */
|
|
5
|
+
export interface StreamOf<Element extends Codec> extends Schema.declare<Stream.Stream<Element["Type"]>> {
|
|
6
|
+
readonly Rebuild: StreamOf<Element>;
|
|
7
|
+
readonly [TypeId]: typeof TypeId;
|
|
8
|
+
/** The schema of each element. */
|
|
9
|
+
readonly element: Element;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A success that is a stream of `element`, a schema or struct fields: the handler returns a
|
|
13
|
+
* `Stream` of them, which may fail with the action's errors, and every client's call returns
|
|
14
|
+
* one. How each surface carries it is on `ActionHttp.layer`, `ActionRpc.make`,
|
|
15
|
+
* `ActionMcp.layerHttp`, `ActionToolkit.make` and `ActionCli.command`.
|
|
16
|
+
* Guide: `docs/Action-streams.md` in this package; in an application: build-events' `tailLog`.
|
|
17
|
+
*
|
|
18
|
+
* **Gotchas**
|
|
19
|
+
*
|
|
20
|
+
* - A tool answers one result, so MCP and a toolkit collect the whole stream before they answer, its elements as one array: a stream that never ends never answers there.
|
|
21
|
+
* - An element breaking its schema is the call's defect, as an answer breaking `success` is.
|
|
22
|
+
* - With `progress`, values and elements are each in their order, not against each other: a value may reach `onProgress` after an element yielded after it.
|
|
23
|
+
*/
|
|
24
|
+
declare const Stream$1: <const E extends Codec | Fields>(element: E) => StreamOf<CodecOf<E>>;
|
|
25
|
+
/** What a handler or a call of an action with success `S` answers: a stream, or one value. */
|
|
26
|
+
export type Answer<S extends Codec, E, R> = [S] extends [never] ? Effect.Effect<never, E, R> : [S] extends [StreamOf<infer Element>] ? Stream.Stream<Element["Type"], E, R> : Effect.Effect<S["Type"], E, R>;
|
|
27
|
+
/**
|
|
28
|
+
* The success a tool answers for an action with success `S`: an array of the elements of one
|
|
29
|
+
* answering a stream, collected whole, or `S`.
|
|
30
|
+
*/
|
|
31
|
+
export type ToolSuccess<S extends Codec> = [S] extends [StreamOf<infer Element>] ? Schema.$Array<Element> : S;
|
|
32
|
+
/** What a call of an action with success `S` gives one at a time: an element, or its answer. */
|
|
33
|
+
export type Each<S extends Codec> = [S] extends [StreamOf<infer Element>] ? Element["Type"] : S["Type"];
|
|
34
|
+
export { Stream$1 as Stream };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { codecOf } from "./codec.mjs";
|
|
2
|
+
import { Predicate, Schema, Stream } from "effect";
|
|
3
|
+
// src/contract/stream.ts
|
|
4
|
+
const TypeId = "~effect-actions/Stream";
|
|
5
|
+
const streams = Schema.declare(Stream.isStream);
|
|
6
|
+
/**
|
|
7
|
+
* A success that is a stream of `element`, a schema or struct fields: the handler returns a
|
|
8
|
+
* `Stream` of them, which may fail with the action's errors, and every client's call returns
|
|
9
|
+
* one. How each surface carries it is on `ActionHttp.layer`, `ActionRpc.make`,
|
|
10
|
+
* `ActionMcp.layerHttp`, `ActionToolkit.make` and `ActionCli.command`.
|
|
11
|
+
* Guide: `docs/Action-streams.md` in this package; in an application: build-events' `tailLog`.
|
|
12
|
+
*
|
|
13
|
+
* **Gotchas**
|
|
14
|
+
*
|
|
15
|
+
* - A tool answers one result, so MCP and a toolkit collect the whole stream before they answer, its elements as one array: a stream that never ends never answers there.
|
|
16
|
+
* - An element breaking its schema is the call's defect, as an answer breaking `success` is.
|
|
17
|
+
* - With `progress`, values and elements are each in their order, not against each other: a value may reach `onProgress` after an element yielded after it.
|
|
18
|
+
*/
|
|
19
|
+
const Stream$1 = (element) => Schema.make(streams.ast, {
|
|
20
|
+
[TypeId]: TypeId,
|
|
21
|
+
element: codecOf(element)
|
|
22
|
+
});
|
|
23
|
+
const isStreamOf = (success) => Predicate.hasProperty(success, TypeId);
|
|
24
|
+
/** The element schema of an action answering a stream, `undefined` for any other action. */
|
|
25
|
+
const elementOf = (success) => isStreamOf(success) ? success.element : void 0;
|
|
26
|
+
/** The success a tool answers for an action with `success`, as `ToolSuccess` types it. */
|
|
27
|
+
const toolSuccessOf = (success) => {
|
|
28
|
+
const element = elementOf(success);
|
|
29
|
+
return element === void 0 ? success : Schema.Array(element);
|
|
30
|
+
};
|
|
31
|
+
export { Stream$1 as Stream, elementOf, toolSuccessOf };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Codec } from "./codec.mjs";
|
|
2
|
+
import { Any } from "../Event.mjs";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
/** What a webhook subscribe to `event` takes: a subscription's input and cursor, and its fields. */
|
|
5
|
+
export declare const WebhookSubscriptionOf: <E extends Any>(event: E) => Schema.Struct<{
|
|
6
|
+
readonly input: Codec;
|
|
7
|
+
readonly cursor: Schema.withDecodingDefaultKey<Schema.NullOr<Schema.String>, never>;
|
|
8
|
+
delivery: Schema.Struct<{
|
|
9
|
+
readonly url: Schema.decodeTo<Schema.URL, Schema.String, never, never>;
|
|
10
|
+
readonly secret: Schema.decodeTo<Schema.Struct<{
|
|
11
|
+
readonly text: Schema.String;
|
|
12
|
+
readonly key: Schema.Uint8Array;
|
|
13
|
+
}>, Schema.String, never, never>;
|
|
14
|
+
}>;
|
|
15
|
+
ttlMs: Schema.optional<Schema.NullOr<Schema.Finite>>;
|
|
16
|
+
}>;
|
|
17
|
+
/** A webhook subscribe's request, its input decoded, as a surface reads it whatever its event. */
|
|
18
|
+
export type AnyWebhookSubscription = ReturnType<typeof WebhookSubscriptionOf<Any>>["Type"];
|
|
19
|
+
/** What a webhook unsubscribe from `event` takes: the input and callback it subscribed with. */
|
|
20
|
+
export declare const WebhookCancellationOf: <E extends Any>(event: E) => Schema.Struct<{
|
|
21
|
+
readonly input: Codec;
|
|
22
|
+
readonly delivery: Schema.Struct<{
|
|
23
|
+
readonly url: Schema.decodeTo<Schema.URL, Schema.String, never, never>;
|
|
24
|
+
}>;
|
|
25
|
+
}>;
|
|
26
|
+
/** A webhook unsubscribe's request, its input decoded, whatever its event. */
|
|
27
|
+
export type AnyWebhookCancellation = ReturnType<typeof WebhookCancellationOf<Any>>["Type"];
|
|
28
|
+
/**
|
|
29
|
+
* A webhook subscription's grant: its id, which each delivery's `X-MCP-Subscription-Id` carries,
|
|
30
|
+
* when to refresh it by, the position it is safe to resume from, and whether delivery starts
|
|
31
|
+
* later than the cursor asked.
|
|
32
|
+
*/
|
|
33
|
+
export declare const Subscribed: Schema.Struct<{
|
|
34
|
+
readonly id: Schema.String;
|
|
35
|
+
readonly refreshBefore: Schema.NullOr<Schema.String>;
|
|
36
|
+
readonly cursor: Schema.NullOr<Schema.String>;
|
|
37
|
+
readonly truncated: Schema.Boolean;
|
|
38
|
+
readonly deliveryStatus: Schema.optional<Schema.Struct<{
|
|
39
|
+
readonly active: Schema.Boolean;
|
|
40
|
+
readonly lastDeliveryAt: Schema.NullOr<Schema.String>;
|
|
41
|
+
readonly lastError: Schema.NullOr<Schema.Literals<readonly ["connection_refused", "timeout", "tls_error", "http_4xx", "http_5xx", "challenge_failed"]>>;
|
|
42
|
+
readonly failedSince: Schema.optional<Schema.String>;
|
|
43
|
+
}>>;
|
|
44
|
+
}>;
|
|
45
|
+
export type Subscribed = typeof Subscribed.Type;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CallbackCategory } from "../Event.mjs";
|
|
2
|
+
import { CallbackUrl, WebhookSecret } from "./callback.mjs";
|
|
3
|
+
import { SubscriptionOf } from "./push.mjs";
|
|
4
|
+
import { Schema, Struct } from "effect";
|
|
5
|
+
// src/contract/subscribe.ts
|
|
6
|
+
/** Where a webhook subscription delivers: its callback, and the secret signing each POST. */
|
|
7
|
+
const WebhookCallback = Schema.Struct({
|
|
8
|
+
url: CallbackUrl,
|
|
9
|
+
secret: WebhookSecret
|
|
10
|
+
});
|
|
11
|
+
/** The callback a webhook unsubscribe names: its URL alone. */
|
|
12
|
+
const WebhookTarget = WebhookCallback.mapFields(Struct.pick(["url"]));
|
|
13
|
+
/**
|
|
14
|
+
* What a webhook subscribe asks beside the subscription it names: its callback, and the lifetime
|
|
15
|
+
* it suggests in milliseconds, `null` asking for none.
|
|
16
|
+
*/
|
|
17
|
+
const webhookFields = {
|
|
18
|
+
delivery: WebhookCallback,
|
|
19
|
+
ttlMs: Schema.optional(Schema.NullOr(Schema.Finite)).annotate({ description: "The lifetime it suggests, in milliseconds; the answer's refreshBefore is the grant." })
|
|
20
|
+
};
|
|
21
|
+
/** What a webhook subscribe to `event` takes: a subscription's input and cursor, and its fields. */
|
|
22
|
+
const WebhookSubscriptionOf = (event) => SubscriptionOf(event).pipe(Schema.fieldsAssign(webhookFields));
|
|
23
|
+
/** What a webhook unsubscribe from `event` takes: the input and callback it subscribed with. */
|
|
24
|
+
const WebhookCancellationOf = (event) => Schema.Struct({
|
|
25
|
+
input: event.input,
|
|
26
|
+
delivery: WebhookTarget
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* How a webhook subscription's deliveries are going, as a refresh answers it: whether it is
|
|
30
|
+
* delivering, when it last delivered, and the category of its last failure, since when it fails.
|
|
31
|
+
*/
|
|
32
|
+
const DeliveryStatus = Schema.Struct({
|
|
33
|
+
active: Schema.Boolean,
|
|
34
|
+
lastDeliveryAt: Schema.NullOr(Schema.String),
|
|
35
|
+
lastError: Schema.NullOr(CallbackCategory),
|
|
36
|
+
failedSince: Schema.optional(Schema.String)
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* A webhook subscription's grant: its id, which each delivery's `X-MCP-Subscription-Id` carries,
|
|
40
|
+
* when to refresh it by, the position it is safe to resume from, and whether delivery starts
|
|
41
|
+
* later than the cursor asked.
|
|
42
|
+
*/
|
|
43
|
+
const Subscribed = Schema.Struct({
|
|
44
|
+
id: Schema.String,
|
|
45
|
+
refreshBefore: Schema.NullOr(Schema.String),
|
|
46
|
+
cursor: Schema.NullOr(Schema.String),
|
|
47
|
+
truncated: Schema.Boolean,
|
|
48
|
+
deliveryStatus: Schema.optional(DeliveryStatus)
|
|
49
|
+
});
|
|
50
|
+
export { DeliveryStatus, Subscribed, WebhookCallback, WebhookCancellationOf, WebhookSubscriptionOf, WebhookTarget, webhookFields };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isTransientStatus } from "./status.mjs";
|
|
2
|
+
import { readOn } from "./push.mjs";
|
|
3
|
+
import { Cause, Duration, Effect, Function, Match, Predicate, Stream } from "effect";
|
|
4
|
+
import { HttpClientError } from "effect/http";
|
|
5
|
+
// src/contract/transient.ts
|
|
6
|
+
/** The status a server answered with, where `error` is a failure to read its answer. */
|
|
7
|
+
const answeredStatus = ({ reason }) => Match.value(reason).pipe(Match.tag("StatusCodeError", "DecodeError", ({ response }) => response.status), Match.orElse(Function.constUndefined));
|
|
8
|
+
/** Whether an HTTP client's failure, by its `reason`, is one a reconnect may outlast. */
|
|
9
|
+
const isTransientReason = (reason) => Match.value(reason).pipe(Match.tag("TransportError", () => true), Match.tag("DecodeError", ({ response }) => response.status < 300 || isTransientStatus(response.status)), Match.tag("StatusCodeError", ({ response }) => isTransientStatus(response.status)), Match.orElse(() => false));
|
|
10
|
+
/** Whether a remote watch's failure is one a reconnect may outlast. */
|
|
11
|
+
const isTransient = Predicate.or(Cause.isTimeoutError, Predicate.compose(HttpClientError.isHttpClientError, ({ reason }) => isTransientReason(reason)));
|
|
12
|
+
const silentFor = Duration.seconds(60);
|
|
13
|
+
/**
|
|
14
|
+
* `request`, failing with a `TimeoutError`, which `isTransient` admits, when it is unanswered for
|
|
15
|
+
* as long as a stream may be silent, such as a server that holds its status back.
|
|
16
|
+
*/
|
|
17
|
+
const failingWhenUnanswered = (request) => Effect.timeout(request, silentFor);
|
|
18
|
+
/** `stream`, failing with a `TimeoutError`, which `isTransient` admits, once it goes silent. */
|
|
19
|
+
const failingWhenSilent = (stream) => stream.pipe(Stream.timeoutOrElse({
|
|
20
|
+
duration: silentFor,
|
|
21
|
+
orElse: () => Stream.fail(new Cause.TimeoutError("The event stream went silent"))
|
|
22
|
+
}));
|
|
23
|
+
/**
|
|
24
|
+
* What a client reads from a push stream of `surface`'s event `name`: each element but `Failed`,
|
|
25
|
+
* which is logged, the server having read on; failing with a `TimeoutError` once it goes silent.
|
|
26
|
+
*/
|
|
27
|
+
const receivedFrom = (surface, name) => (sent) => sent.pipe(failingWhenSilent, readOn(surface, name));
|
|
28
|
+
export { answeredStatus, failingWhenSilent, failingWhenUnanswered, isTransient, isTransientReason, receivedFrom };
|
|
@@ -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 };
|