@gjermundgaraba/effect-actions 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -41
- package/dist/Action.d.mts +140 -48
- package/dist/Action.mjs +88 -44
- package/dist/ActionCli.d.mts +70 -35
- package/dist/ActionCli.mjs +53 -34
- package/dist/ActionHttp.d.mts +70 -38
- package/dist/ActionHttp.mjs +76 -25
- package/dist/ActionMcp.d.mts +56 -24
- package/dist/ActionMcp.mjs +104 -61
- package/dist/ActionRpc.d.mts +222 -57
- package/dist/ActionRpc.mjs +301 -49
- package/dist/ActionToolkit.d.mts +22 -7
- package/dist/ActionToolkit.mjs +37 -14
- package/dist/Authentication.d.mts +42 -12
- package/dist/Authentication.mjs +38 -32
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.d.mts +1 -3
- package/dist/Testing.mjs +2 -3
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -12
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -4
- package/dist/cli/commands.d.mts +9 -7
- package/dist/cli/commands.mjs +85 -35
- package/dist/contract/ast.mjs +5 -3
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -8
- package/dist/contract/call.mjs +49 -8
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +12 -3
- package/dist/contract/caller.mjs +23 -5
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +26 -11
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -5
- package/dist/contract/errors.mjs +2 -3
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hints.mjs +1 -2
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -51
- package/dist/contract/implementation.mjs +94 -60
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/media.d.mts +1 -3
- package/dist/contract/media.mjs +1 -2
- package/dist/contract/names.mjs +15 -6
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +15 -7
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -14
- package/dist/http/client.mjs +52 -14
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +2 -3
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -99
- package/dist/mcp/client.mjs +60 -134
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +40 -15
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +14 -7
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +4 -5
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +57 -61
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -53
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +25 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +19 -17
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { literalValues, members } from "./ast.mjs";
|
|
2
1
|
import { Anyone, assertAuthentication } from "./caller.mjs";
|
|
2
|
+
import { literalValues, members } from "./ast.mjs";
|
|
3
3
|
import { builtIns } from "./errors.mjs";
|
|
4
|
-
import { assertOnce } from "./names.mjs";
|
|
5
4
|
import { httpApiStatusOf } from "./status.mjs";
|
|
6
5
|
import { Predicate, Schema, SchemaAST } from "effect";
|
|
7
|
-
|
|
8
|
-
const errorList = (error) =>
|
|
6
|
+
// src/contract/declared.ts
|
|
7
|
+
const errorList = (error) => {
|
|
8
|
+
if (error === void 0) return [];
|
|
9
|
+
return Schema.isSchema(error) ? [error] : error;
|
|
10
|
+
};
|
|
9
11
|
const ownsNothing = (ast) => httpApiStatusOf(ast) === void 0 && ast.checks === void 0 && ast.encoding === void 0;
|
|
10
|
-
const schemasOf = (ast) =>
|
|
12
|
+
const schemasOf = (ast) => {
|
|
13
|
+
if (!ownsNothing(ast)) return [ast];
|
|
14
|
+
if (SchemaAST.isSuspend(ast)) return schemasOf(ast.thunk());
|
|
15
|
+
return SchemaAST.isUnion(ast) ? ast.types.flatMap(schemasOf) : [ast];
|
|
16
|
+
};
|
|
11
17
|
/** `errors` as the schemas a call decodes a failure with, in order, each once. */
|
|
12
18
|
const errorSchemas = (errors) => {
|
|
13
19
|
const schemas = /* @__PURE__ */ new Map();
|
|
@@ -24,6 +30,14 @@ const projectedErrors = (action, surface = []) => errorSchemas([
|
|
|
24
30
|
* binding's and, where `action` is protected, its authentication's.
|
|
25
31
|
*/
|
|
26
32
|
const bindingErrors = (action, binding) => projectedErrors(action, action.caller === Anyone || binding.authentication === void 0 ? binding.error : [...binding.error, ...binding.authentication.error]);
|
|
33
|
+
/**
|
|
34
|
+
* Every error a binding's subscription to `event` declares: the built-in ones, the binding's and,
|
|
35
|
+
* where `event` is protected, its authentication's. An event type declares none of its own.
|
|
36
|
+
*/
|
|
37
|
+
const eventErrors = (event, binding) => bindingErrors({
|
|
38
|
+
error: [],
|
|
39
|
+
caller: event.caller
|
|
40
|
+
}, binding);
|
|
27
41
|
const encodedTags = (ast) => members(SchemaAST.toEncoded(ast)).flatMap((member) => {
|
|
28
42
|
const tag = SchemaAST.isObjects(member) ? member.propertySignatures.find((property) => property.name === "_tag")?.type : void 0;
|
|
29
43
|
return tag === void 0 ? [] : members(tag).flatMap(literalValues).filter(Predicate.isString);
|
|
@@ -38,18 +52,19 @@ const assertDistinctTags = (what, errors) => {
|
|
|
38
52
|
}
|
|
39
53
|
};
|
|
40
54
|
/**
|
|
41
|
-
* The checks `ActionHttp.make` and `ActionRpc.make` share:
|
|
42
|
-
*
|
|
55
|
+
* The checks `ActionHttp.make` and `ActionRpc.make` share: the descriptor every protected action
|
|
56
|
+
* and event type names, and each call's and each subscription's errors distinct by `_tag`.
|
|
57
|
+
* Returns the binding's `error`, as a list, and `authentication`.
|
|
43
58
|
*/
|
|
44
|
-
const bindingOf = (what, actions, options) => {
|
|
45
|
-
assertOnce("action", actions);
|
|
59
|
+
const bindingOf = (what, actions, events, options) => {
|
|
46
60
|
const bound = {
|
|
47
61
|
error: errorList(options.error),
|
|
48
62
|
authentication: options.authentication
|
|
49
63
|
};
|
|
50
64
|
assertAuthentication(actions, bound.authentication);
|
|
65
|
+
assertAuthentication(events, bound.authentication, "event");
|
|
51
66
|
for (const action of actions) assertDistinctTags(`${what} of "${action.name}"`, bindingErrors(action, bound));
|
|
67
|
+
for (const event of events) assertDistinctTags(`${what} of event "${event.name}"`, eventErrors(event, bound));
|
|
52
68
|
return bound;
|
|
53
69
|
};
|
|
54
|
-
|
|
55
|
-
export { assertDistinctTags, bindingErrors, bindingOf, errorList, errorSchemas, projectedErrors };
|
|
70
|
+
export { assertDistinctTags, bindingErrors, bindingOf, errorList, errorSchemas, eventErrors, projectedErrors };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
// src/contract/digest.ts
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
/** The SHA-256 of `text`, in hex. */
|
|
5
|
+
const sha256Hex = (text) => Effect.promise(async () => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(text))), (byte) => byte.toString(16).padStart(2, "0")).join(""));
|
|
6
|
+
export { sha256Hex };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
//#region src/contract/errors.d.ts
|
|
3
2
|
declare const Issue: Schema.Struct<{
|
|
4
3
|
readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
|
|
5
4
|
readonly message: Schema.String;
|
|
@@ -16,7 +15,9 @@ declare const InvalidInput_base: Schema.Class<InvalidInput, Schema.TaggedStruct<
|
|
|
16
15
|
readonly path: Schema.$Array<Schema.Union<readonly [Schema.String, Schema.Finite]>>;
|
|
17
16
|
readonly message: Schema.String;
|
|
18
17
|
}>>>;
|
|
19
|
-
}>, import("effect/Cause").YieldableError
|
|
18
|
+
}>, import("effect/Cause").YieldableError & {
|
|
19
|
+
readonly brand: unique symbol;
|
|
20
|
+
}>;
|
|
20
21
|
/**
|
|
21
22
|
* The request's input does not decode against the action's input: its message is the schema's
|
|
22
23
|
* own description of what is wrong, and `issues` the same an issue at a time, for a caller that
|
|
@@ -45,7 +46,9 @@ export declare class InvalidInput extends InvalidInput_base {
|
|
|
45
46
|
}
|
|
46
47
|
declare const Unauthenticated_base: Schema.Class<Unauthenticated, Schema.TaggedStruct<"Unauthenticated", {
|
|
47
48
|
readonly message: Schema.withConstructorDefault<Schema.String>;
|
|
48
|
-
}>, import("effect/Cause").YieldableError
|
|
49
|
+
}>, import("effect/Cause").YieldableError & {
|
|
50
|
+
readonly brand: unique symbol;
|
|
51
|
+
}>;
|
|
49
52
|
/**
|
|
50
53
|
* The caller is not authenticated: authentication refuses the credential, or an implementation's
|
|
51
54
|
* `authorize` or a handler fails with it.
|
|
@@ -63,7 +66,9 @@ export declare class Unauthenticated extends Unauthenticated_base {
|
|
|
63
66
|
declare const Forbidden_base: Schema.Class<Forbidden, Schema.TaggedStruct<"Forbidden", {
|
|
64
67
|
readonly message: Schema.withConstructorDefault<Schema.String>;
|
|
65
68
|
readonly scopes: Schema.optionalKey<Schema.NonEmptyArray<Schema.String>>;
|
|
66
|
-
}>, import("effect/Cause").YieldableError
|
|
69
|
+
}>, import("effect/Cause").YieldableError & {
|
|
70
|
+
readonly brand: unique symbol;
|
|
71
|
+
}>;
|
|
67
72
|
/**
|
|
68
73
|
* The caller may not run this action: authentication refuses the credential, an
|
|
69
74
|
* implementation's `authorize` refuses, or a handler fails with it. `scopes`, when given, are every OAuth scope the call needs: a refusal naming
|
|
@@ -100,5 +105,4 @@ export type BuiltIns = (typeof builtIns)[number];
|
|
|
100
105
|
export declare const BuiltIn: Schema.Union<readonly [typeof InvalidInput, typeof Unauthenticated, typeof Forbidden]>;
|
|
101
106
|
/** A built-in failure: `InvalidInput | Refusal`. */
|
|
102
107
|
export type BuiltIn = (typeof BuiltIn)["Type"];
|
|
103
|
-
//#endregion
|
|
104
108
|
export { Issue };
|
package/dist/contract/errors.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect, Predicate, Schema, SchemaIssue } from "effect";
|
|
2
|
-
|
|
2
|
+
// src/contract/errors.ts
|
|
3
3
|
const defaultedMessage = (fallback) => Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed(fallback)));
|
|
4
4
|
const Issue = Schema.Struct({
|
|
5
5
|
path: Schema.Array(Schema.Union([Schema.String, Schema.Finite])),
|
|
@@ -55,7 +55,7 @@ var InvalidInput = class InvalidInput extends Schema.TaggedError()("InvalidInput
|
|
|
55
55
|
* - From another service's client, such as `ActionHttp.client`, one passed on tells a caller whose token is valid to sign in again: map it to an error the action declares, or to a defect.
|
|
56
56
|
*/
|
|
57
57
|
var Unauthenticated = class extends Schema.TaggedError()("Unauthenticated", { message: defaultedMessage("Authentication is required.") }, { httpApiStatus: 401 }) {};
|
|
58
|
-
const scopeToken = /^[\
|
|
58
|
+
const scopeToken = /^[\u0021\u0023-\u005B\u005D-\u007E]+$/;
|
|
59
59
|
const ScopeToken = Schema.String.check(Schema.isPattern(scopeToken));
|
|
60
60
|
/**
|
|
61
61
|
* The caller may not run this action: authentication refuses the credential, an
|
|
@@ -89,5 +89,4 @@ const Refusal = Schema.Union(refusals);
|
|
|
89
89
|
const builtIns = [InvalidInput, ...refusals];
|
|
90
90
|
/** The built-in failures as one schema: `Schema.is(Action.BuiltIn)(error)`. */
|
|
91
91
|
const BuiltIn = Schema.Union(builtIns);
|
|
92
|
-
//#endregion
|
|
93
92
|
export { BuiltIn, Forbidden, InvalidInput, Refusal, Unauthenticated, builtIns, scopeToken };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { Anyone } from "./caller.mjs";
|
|
2
|
+
import { Fed, Page } from "./pages.mjs";
|
|
3
|
+
import { Any } from "../Event.mjs";
|
|
4
|
+
import { ErasedAuthorize, ErasedValue, Memoized, Protected } from "./implementation.mjs";
|
|
5
|
+
import { Context, Duration, Effect, Layer, Stream } from "effect";
|
|
6
|
+
/**
|
|
7
|
+
* An implemented event type, behind its authorizer, as a surface serves it: a poll, whose
|
|
8
|
+
* subscription `subscriber` names, such as its principal, and a stream, which lasts as long as it
|
|
9
|
+
* is read. Both authenticate and authorize the subscriber first, and run as it.
|
|
10
|
+
*/
|
|
11
|
+
export interface Feed {
|
|
12
|
+
readonly event: Any;
|
|
13
|
+
/**
|
|
14
|
+
* Releases the `subscription` hook `subscriber`'s polls of `input` hold: present exactly where
|
|
15
|
+
* its source has a hook, held per subscriber.
|
|
16
|
+
*/
|
|
17
|
+
readonly release?: ((input: ErasedValue, subscriber: string) => Effect.Effect<void>) | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Authenticates the subscriber, checks `input` by `Action.InputCheck` and authorizes it, as
|
|
20
|
+
* `poll` and `stream` do first.
|
|
21
|
+
*/
|
|
22
|
+
readonly admit: (input: ErasedValue) => Effect.Effect<void, unknown, unknown>;
|
|
23
|
+
readonly poll: (input: ErasedValue, cursor: string | null, options: PollOptions) => Effect.Effect<Page, unknown, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* Admits the subscriber, then reads the feed from a cursor, as often as a subscription
|
|
26
|
+
* reopens it.
|
|
27
|
+
*/
|
|
28
|
+
readonly stream: (input: ErasedValue) => Effect.Effect<(cursor: string | null) => Stream.Stream<Fed, unknown, unknown>, unknown, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/** Who polls, and how many occurrences at most a log source's page holds. */
|
|
31
|
+
export interface PollOptions {
|
|
32
|
+
readonly subscriber?: string | undefined;
|
|
33
|
+
/** Capped at the page a log source reads anyway; a check's page is answered whole. */
|
|
34
|
+
readonly maxEvents?: number | undefined;
|
|
35
|
+
}
|
|
36
|
+
/** What reads one event type's source, before its subscriber is admitted. */
|
|
37
|
+
export interface Run extends Omit<Feed, "event" | "stream" | "admit"> {
|
|
38
|
+
readonly stream: (input: ErasedValue, cursor: string | null) => Stream.Stream<Fed, unknown, unknown>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Each event of an implementation with what runs its source, each built on its own over the
|
|
42
|
+
* implementation's one builder, so a selection runs only the sources of the events it holds.
|
|
43
|
+
*/
|
|
44
|
+
export type Running<E = unknown, R = unknown> = ReadonlyArray<readonly [Any, Memoized<Run, E, R>]>;
|
|
45
|
+
/**
|
|
46
|
+
* Event types bound to their sources and their authorizer: everything one `Event.implement` call
|
|
47
|
+
* produced. Its type parameters may be written out, as a package emitting declarations prints
|
|
48
|
+
* them: `E` its event types, `R` what its sources read per subscription, by event name, `EX` and `RX` the
|
|
49
|
+
* failures and services of its builder, `RA` what its authorizer reads per subscription, and
|
|
50
|
+
* `EAX` and `RAX` the failures and services of the authorizer's builder.
|
|
51
|
+
*/
|
|
52
|
+
export declare class EventImplementation<E extends Any, R, EX, RX, RA = never, EAX = never, RAX = never> {
|
|
53
|
+
#private;
|
|
54
|
+
/** Tells an event implementation from an action implementation among what a surface is given. */
|
|
55
|
+
readonly _tag = "EventImplementation";
|
|
56
|
+
/** Type-only: what its sources read per subscription, by event name. */
|
|
57
|
+
readonly "~eventRequest": R;
|
|
58
|
+
/** Type-only: what building its sources fails with. */
|
|
59
|
+
readonly "~eventBuildError": EX;
|
|
60
|
+
/** Type-only: what building its sources needs. */
|
|
61
|
+
readonly "~eventBuildServices": RX;
|
|
62
|
+
/** Type-only: what its authorizer reads per subscription. */
|
|
63
|
+
readonly "~eventAuthorizeRequest": RA;
|
|
64
|
+
/** Type-only: what building its authorizer fails with. */
|
|
65
|
+
readonly "~eventAuthorizeBuildError": EAX;
|
|
66
|
+
/** Type-only: what building its authorizer needs. */
|
|
67
|
+
readonly "~eventAuthorizeBuildServices": RAX;
|
|
68
|
+
/** The event types this implementation answers. */
|
|
69
|
+
readonly events: readonly E[];
|
|
70
|
+
constructor(events: readonly E[], running: Running<EX, RX>, authorizer: Memoized<ErasedAuthorize<Any>, EAX, RAX>);
|
|
71
|
+
/** Select event types while retaining their sources, authorization and shared builder. */
|
|
72
|
+
static share(events: readonly Any[], app: AnyEventImplementation): AnyEventImplementation;
|
|
73
|
+
/** The builders of `app`, its sources' and its authorizer's, as one layer. */
|
|
74
|
+
static layerOf(app: AnyEventImplementation): Layer.Layer<never, unknown, unknown>;
|
|
75
|
+
/** The event types of `app` with what runs them, behind its authorizer. */
|
|
76
|
+
static feedsOf(app: AnyEventImplementation): Effect.Effect<readonly Feed[], never, unknown>;
|
|
77
|
+
/** What runs each event type `app` holds. */
|
|
78
|
+
private static held;
|
|
79
|
+
private static own;
|
|
80
|
+
}
|
|
81
|
+
/** Any event implementation, with its events and channels erased. */
|
|
82
|
+
export type AnyEventImplementation<E extends Any = Any> = EventImplementation<E, unknown, unknown, unknown, unknown, unknown, unknown>;
|
|
83
|
+
/** The event types of the implementations `App`. */
|
|
84
|
+
export type EventOf<App> = App extends {
|
|
85
|
+
readonly events: ReadonlyArray<infer E extends Any>;
|
|
86
|
+
} ? E : never;
|
|
87
|
+
type IdentityOf<K> = K extends Context.Key<infer I, unknown> ? I : never;
|
|
88
|
+
type RequestOf<R, Name> = Name extends keyof R ? R[Name] : never;
|
|
89
|
+
/**
|
|
90
|
+
* What a subscription of `E` owes per subscriber where the implementations `App` serve it: its
|
|
91
|
+
* sources' services, and for a protected event its authorizer's and the subscriber's identity.
|
|
92
|
+
*/
|
|
93
|
+
export type EventRequestOf<App, E extends Any> = App extends {
|
|
94
|
+
readonly events: ReadonlyArray<infer Held>;
|
|
95
|
+
readonly "~eventRequest": infer R;
|
|
96
|
+
readonly "~eventAuthorizeRequest": infer RA;
|
|
97
|
+
} ? E extends Held ? RequestOf<R, E["name"]> | (E["caller"] extends typeof Anyone ? never : RA | IdentityOf<E["caller"]>) : never : never;
|
|
98
|
+
/**
|
|
99
|
+
* The event types of `App` a surface given the event types `Listed` serves, as `selectEvents`
|
|
100
|
+
* selects them by identity: one whose type either is assignable to, or is assigned by, one
|
|
101
|
+
* listed, so an implementation or a list typed wider than its event types still counts.
|
|
102
|
+
*/
|
|
103
|
+
export type ServingEvents<App, Listed extends Any> = EventOf<App> extends (infer E extends Any) ? E extends unknown ? [E] extends [Listed] ? E : [Extract<Listed, E>] extends [never] ? never : E : never : never;
|
|
104
|
+
/** The event implementations among `App` holding an event type of `Listed`: those a surface builds. */
|
|
105
|
+
export type HoldingEvents<App, Listed extends Any> = App extends unknown ? [ServingEvents<App, Listed>] extends [never] ? never : App : never;
|
|
106
|
+
type ServesProtected<App, Listed extends Any> = [Protected<ServingEvents<App, Listed>>] extends [never] ? false : true;
|
|
107
|
+
/**
|
|
108
|
+
* What a remote surface's requests owe the event implementations `App` per subscription where it
|
|
109
|
+
* serves their event types of `Listed`: their sources' services, and their authorizer's where a
|
|
110
|
+
* served one is protected; the identity is the endpoint's own to provide.
|
|
111
|
+
*/
|
|
112
|
+
export type EventServedRequest<App, Listed extends Any = EventOf<App>> = App extends {
|
|
113
|
+
readonly "~eventRequest": infer R;
|
|
114
|
+
readonly "~eventAuthorizeRequest": infer RA;
|
|
115
|
+
} ? [ServingEvents<App, Listed>] extends [never] ? never : EndpointRequest<R, ServingEvents<App, Listed>> | (ServesProtected<App, Listed> extends true ? Exclude<RA, IdentityOf<Protected<ServingEvents<App, Listed>>["caller"]>> : never) : never;
|
|
116
|
+
type EndpointRequest<R, E extends Any> = E extends unknown ? E["caller"] extends typeof Anyone ? RequestOf<R, E["name"]> : Exclude<RequestOf<R, E["name"]>, IdentityOf<E["caller"]>> : never;
|
|
117
|
+
/** What building the event implementations `App` fails with, where their event types of `Listed` are served. */
|
|
118
|
+
export type EventBuildError<App, Listed extends Any = EventOf<App>> = App extends {
|
|
119
|
+
readonly "~eventBuildError": infer EX;
|
|
120
|
+
readonly "~eventAuthorizeBuildError": infer EAX;
|
|
121
|
+
} ? [ServingEvents<App, Listed>] extends [never] ? never : EX | (ServesProtected<App, Listed> extends true ? EAX : never) : never;
|
|
122
|
+
/** What building the event implementations `App` reads at startup, where their event types of `Listed` are served. */
|
|
123
|
+
export type EventBuildServices<App, Listed extends Any = EventOf<App>> = App extends {
|
|
124
|
+
readonly "~eventBuildServices": infer RX;
|
|
125
|
+
readonly "~eventAuthorizeBuildServices": infer RAX;
|
|
126
|
+
} ? [ServingEvents<App, Listed>] extends [never] ? never : RX | (ServesProtected<App, Listed> extends true ? RAX : never) : never;
|
|
127
|
+
export {};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { Anyone } from "./caller.mjs";
|
|
2
|
+
import { Unauthenticated } from "./errors.mjs";
|
|
3
|
+
import { checkInput } from "./input-check.mjs";
|
|
4
|
+
import { assertKnown } from "./names.mjs";
|
|
5
|
+
import { builderLayer, memoized, notMadeHere, selectHeld } from "./implementation.mjs";
|
|
6
|
+
import { jsonRoundTrip } from "./call.mjs";
|
|
7
|
+
import { canonicalInput } from "./canonical.mjs";
|
|
8
|
+
import { reported } from "./reported.mjs";
|
|
9
|
+
import { Events, attachmentsOf, reaches } from "./hub.mjs";
|
|
10
|
+
import { makeLeases } from "./lease.mjs";
|
|
11
|
+
import { AnyOccurrence, Gap } from "./occurrence.mjs";
|
|
12
|
+
import { fedOf, pagedFrom } from "./pages.mjs";
|
|
13
|
+
import { Array, DateTime, Duration, Effect, Layer, Option, Predicate, Stream } from "effect";
|
|
14
|
+
// src/contract/feed.ts
|
|
15
|
+
const defaultNextPoll = Duration.seconds(5);
|
|
16
|
+
const longestNextPoll = Duration.minutes(5);
|
|
17
|
+
const logPageSize = 100;
|
|
18
|
+
const pageLimit = (maxEvents) => Math.min(Math.max(Math.floor(maxEvents), 1), logPageSize);
|
|
19
|
+
const occurrencesOf = (event) => {
|
|
20
|
+
const sent = jsonRoundTrip(event.payload);
|
|
21
|
+
return (emitted, cursor) => Effect.matchCauseEffect(sent(emitted.data), {
|
|
22
|
+
onFailure: (cause) => Effect.as(Effect.annotateLogs(reported(cause), "event", event.name), Gap.make({ cursor })),
|
|
23
|
+
onSuccess: ({ value }) => Effect.map(DateTime.now, (now) => AnyOccurrence.make({
|
|
24
|
+
eventId: emitted.eventId ?? globalThis.crypto.randomUUID(),
|
|
25
|
+
name: event.name,
|
|
26
|
+
timestamp: emitted.timestamp ?? now,
|
|
27
|
+
data: value,
|
|
28
|
+
cursor
|
|
29
|
+
}))
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const isOccurrence = (fed) => Predicate.isTagged(fed, "Occurrence");
|
|
33
|
+
/** A poll's page of `items`: their occurrences, `truncated` too where a gap stands for one. */
|
|
34
|
+
const pageOf = (items, { truncated, ...page }) => {
|
|
35
|
+
const events = items.filter(isOccurrence);
|
|
36
|
+
return {
|
|
37
|
+
...page,
|
|
38
|
+
events,
|
|
39
|
+
truncated: truncated || events.length < items.length
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const lifted = (value) => Effect.isEffect(value) ? value : Effect.succeed(value);
|
|
43
|
+
/** The key a subscriber of `input` is reached by, as the source's `key` answers it. */
|
|
44
|
+
const keyOf = (source, input) => source.key === void 0 ? Effect.undefined : lifted(source.key(input));
|
|
45
|
+
const leaseKey = (event, subscriber, input) => Effect.map(canonicalInput(event.input, input), (encoded) => `${subscriber ?? ""}\u0000${encoded}`);
|
|
46
|
+
const answeredPage = ({ answered, items }) => pageOf(items, {
|
|
47
|
+
cursor: answered.cursor,
|
|
48
|
+
truncated: answered.truncated === true,
|
|
49
|
+
hasMore: answered.hasMore === true,
|
|
50
|
+
nextPoll: Duration.min(Duration.fromInputUnsafe(answered.nextPoll ?? defaultNextPoll), longestNextPoll)
|
|
51
|
+
});
|
|
52
|
+
const checkRun = (event, source, { wakes, polls }) => {
|
|
53
|
+
const occurrenceOf = occurrencesOf(event);
|
|
54
|
+
const read = (input, cursor) => Effect.flatMap(source.check(input, cursor), (answered) => Effect.map(Effect.forEach(answered.events, (emitted, index) => occurrenceOf(emitted, index === answered.events.length - 1 ? answered.cursor : cursor)), (items) => ({
|
|
55
|
+
answered,
|
|
56
|
+
items
|
|
57
|
+
})));
|
|
58
|
+
return {
|
|
59
|
+
poll: (input, cursor) => Effect.map(read(input, cursor), answeredPage),
|
|
60
|
+
stream: (input, cursor) => Stream.unwrap(Effect.map(Effect.flatMap(keyOf(source, input), wakes), (woken) => pagedFrom((at) => Effect.map(read(input, at), (delivered) => ({
|
|
61
|
+
...answeredPage(delivered),
|
|
62
|
+
fed: fedOf({
|
|
63
|
+
items: delivered.items,
|
|
64
|
+
truncated: delivered.answered.truncated === true,
|
|
65
|
+
from: at,
|
|
66
|
+
to: delivered.answered.cursor
|
|
67
|
+
})
|
|
68
|
+
})), cursor, woken, polls)))
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const currentRun = (event, source, checking) => checkRun(event, {
|
|
72
|
+
key: source.key,
|
|
73
|
+
check: (input, cursor) => Effect.gen(function* () {
|
|
74
|
+
const at = yield* checking.revision(yield* keyOf(source, input));
|
|
75
|
+
if (cursor === at) return {
|
|
76
|
+
cursor: at,
|
|
77
|
+
events: []
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
cursor: at,
|
|
81
|
+
events: [{
|
|
82
|
+
eventId: at,
|
|
83
|
+
data: yield* source.current(input)
|
|
84
|
+
}]
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
}, checking);
|
|
88
|
+
const logRun = (event, source, log) => {
|
|
89
|
+
const occurrenceOf = occurrencesOf(event);
|
|
90
|
+
const delivered = (input, key, { cursor, at, value: logged }) => Effect.gen(function* () {
|
|
91
|
+
if (logged === void 0) {
|
|
92
|
+
yield* Effect.annotateLogs(Effect.logWarning(`An entry of the log of ${event.name} could not be read: sent as a gap`), "event", event.name);
|
|
93
|
+
return Gap.make({ cursor });
|
|
94
|
+
}
|
|
95
|
+
const emitted = logged.value;
|
|
96
|
+
if (!reaches(logged.key, key)) return;
|
|
97
|
+
if (source.match !== void 0 && !(yield* lifted(source.match(input, emitted.data)))) return;
|
|
98
|
+
const data = source.transform === void 0 ? emitted.data : yield* lifted(source.transform(input, emitted.data));
|
|
99
|
+
return yield* occurrenceOf({
|
|
100
|
+
data,
|
|
101
|
+
eventId: emitted.eventId ?? cursor,
|
|
102
|
+
timestamp: emitted.timestamp ?? DateTime.makeUnsafe(at)
|
|
103
|
+
}, cursor);
|
|
104
|
+
});
|
|
105
|
+
const received = (input, key, read) => Effect.map(Effect.forEach(read.entries, (entry) => delivered(input, key, entry)), (items) => items.filter(Predicate.isNotUndefined));
|
|
106
|
+
return {
|
|
107
|
+
poll: (input, cursor, { maxEvents = logPageSize }) => Effect.flatMap(Effect.all([keyOf(source, input), log.read(cursor, pageLimit(maxEvents))]), ([key, read]) => Effect.map(received(input, key, read), (items) => pageOf(items, {
|
|
108
|
+
cursor: read.cursor,
|
|
109
|
+
truncated: read.truncated,
|
|
110
|
+
hasMore: read.hasMore,
|
|
111
|
+
nextPoll: defaultNextPoll
|
|
112
|
+
}))),
|
|
113
|
+
stream: (input, cursor) => Stream.unwrap(Effect.map(keyOf(source, input), (key) => log.follow(cursor, logPageSize).pipe(Stream.mapEffect((read) => Effect.map(received(input, key, read), (items) => fedOf({
|
|
114
|
+
items,
|
|
115
|
+
truncated: read.truncated,
|
|
116
|
+
from: read.after,
|
|
117
|
+
to: read.cursor
|
|
118
|
+
}))), Stream.flattenIterable)))
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
/** `run` with the source's `subscription` held while each subscription lasts. */
|
|
122
|
+
const hooked = (event, run, subscription, leases) => ({
|
|
123
|
+
...run,
|
|
124
|
+
poll: (input, cursor, options) => Effect.flatMap(leaseKey(event, options.subscriber, input), (key) => Effect.andThen(leases.hold(key, input), () => run.poll(input, cursor, options))),
|
|
125
|
+
release: (input, subscriber) => Effect.flatMap(leaseKey(event, subscriber, input), leases.release),
|
|
126
|
+
stream: (input, cursor) => Stream.unwrap(Effect.map(subscription(input), () => run.stream(input, cursor)))
|
|
127
|
+
});
|
|
128
|
+
const isCheck = (source) => Predicate.isFunction(source.check);
|
|
129
|
+
const isCurrent = (source) => Predicate.isFunction(source.current);
|
|
130
|
+
const runOf = (event, source) => Effect.gen(function* () {
|
|
131
|
+
const attach = yield* Effect.flatMap(Events, attachmentsOf);
|
|
132
|
+
let run;
|
|
133
|
+
if (isCheck(source)) run = checkRun(event, source, yield* attach.check(event, false));
|
|
134
|
+
else if (isCurrent(source)) run = currentRun(event, source, yield* attach.check(event, true));
|
|
135
|
+
else run = logRun(event, source, yield* attach.log(event, source.log, source.log.emit));
|
|
136
|
+
if (source.subscription === void 0) return run;
|
|
137
|
+
return hooked(event, run, source.subscription, yield* makeLeases(source.subscription, source.lease));
|
|
138
|
+
});
|
|
139
|
+
const isSource = (source) => {
|
|
140
|
+
const log = Predicate.hasProperty(source, "log") ? source.log : void 0;
|
|
141
|
+
const logged = Predicate.isObject(log) && (!Predicate.hasProperty(log, "emit") || log.emit === void 0 || Stream.isStream(log.emit));
|
|
142
|
+
return [
|
|
143
|
+
Predicate.isFunction(Predicate.hasProperty(source, "check") ? source.check : void 0),
|
|
144
|
+
Predicate.isFunction(Predicate.hasProperty(source, "current") ? source.current : void 0),
|
|
145
|
+
logged
|
|
146
|
+
].filter(Boolean).length === 1;
|
|
147
|
+
};
|
|
148
|
+
const assertSource = (event, source) => {
|
|
149
|
+
if (!isSource(source)) throw new Error(`Event "${event.name}": its source needs exactly one of check, current or log (give check or current a function, or log its options, {} or { emit: stream })`);
|
|
150
|
+
return source;
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* The runtime of `events`' sources: one per event, from the source given for one event alone, or
|
|
154
|
+
* from a record keyed by event name, the builder run once for all of them.
|
|
155
|
+
*/
|
|
156
|
+
const sourcesOf = (events, single, sources) => {
|
|
157
|
+
const names = events.map((event) => event.name);
|
|
158
|
+
const checked = (built) => {
|
|
159
|
+
const keyed = single ? Object.fromEntries(names.map((name) => [name, built])) : Object.fromEntries(Predicate.isObject(built) ? Object.entries(built) : []);
|
|
160
|
+
assertKnown("sources", Object.keys(keyed), names, "remove it, or list its event");
|
|
161
|
+
const missing = names.filter((name) => !Object.hasOwn(keyed, name));
|
|
162
|
+
if (missing.length > 0) throw new Error(`Missing sources: ${missing.join(", ")} (give the source of one event, or a record with an own source per listed event)`);
|
|
163
|
+
return keyed;
|
|
164
|
+
};
|
|
165
|
+
const built = memoized(Effect.isEffect(sources) ? Effect.map(sources, checked) : Effect.sync(() => checked(sources)));
|
|
166
|
+
return events.map((event) => {
|
|
167
|
+
const run = memoized(Effect.flatMap(Effect.service(built.key), (keyed) => runOf(event, assertSource(event, keyed[event.name]))));
|
|
168
|
+
return [event, {
|
|
169
|
+
key: run.key,
|
|
170
|
+
layer: Layer.provide(run.layer, built.layer)
|
|
171
|
+
}];
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
const guarded = (event, run, before) => {
|
|
175
|
+
const attributes = { "event.name": event.name };
|
|
176
|
+
const admit = (input) => event.caller === Anyone ? checkInput(event, input) : Effect.flatMap(Effect.serviceOption(event.caller), (subscriber) => Option.isSome(subscriber) ? Effect.andThen(checkInput(event, input), Effect.suspend(() => before(event))) : Effect.fail(new Unauthenticated()));
|
|
177
|
+
return {
|
|
178
|
+
event,
|
|
179
|
+
release: run.release,
|
|
180
|
+
admit,
|
|
181
|
+
poll: (input, cursor, options) => Effect.andThen(admit(input), () => run.poll(input, cursor, options)).pipe(Effect.annotateLogs(attributes), Effect.withSpan(event.name, { attributes }, { captureStackTrace: false })),
|
|
182
|
+
stream: (input) => Effect.as(admit(input), (cursor) => run.stream(input, cursor).pipe(Stream.withSpan(event.name, { attributes })))
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Event types bound to their sources and their authorizer: everything one `Event.implement` call
|
|
187
|
+
* produced. Its type parameters may be written out, as a package emitting declarations prints
|
|
188
|
+
* them: `E` its event types, `R` what its sources read per subscription, by event name, `EX` and `RX` the
|
|
189
|
+
* failures and services of its builder, `RA` what its authorizer reads per subscription, and
|
|
190
|
+
* `EAX` and `RAX` the failures and services of the authorizer's builder.
|
|
191
|
+
*/
|
|
192
|
+
var EventImplementation = class EventImplementation {
|
|
193
|
+
/** Tells an event implementation from an action implementation among what a surface is given. */
|
|
194
|
+
_tag = "EventImplementation";
|
|
195
|
+
/** The event types this implementation answers. */
|
|
196
|
+
events;
|
|
197
|
+
#running;
|
|
198
|
+
#authorizer;
|
|
199
|
+
constructor(events, running, authorizer) {
|
|
200
|
+
this.events = events;
|
|
201
|
+
this.#running = running;
|
|
202
|
+
this.#authorizer = authorizer;
|
|
203
|
+
}
|
|
204
|
+
/** Select event types while retaining their sources, authorization and shared builder. */
|
|
205
|
+
static share(events, app) {
|
|
206
|
+
const source = EventImplementation.own(app);
|
|
207
|
+
return new EventImplementation(events, source.#running, source.#authorizer);
|
|
208
|
+
}
|
|
209
|
+
/** The builders of `app`, its sources' and its authorizer's, as one layer. */
|
|
210
|
+
static layerOf(app) {
|
|
211
|
+
const source = EventImplementation.own(app);
|
|
212
|
+
const runs = Layer.mergeAll(Layer.empty, ...EventImplementation.held(app).map(([, run]) => run.layer));
|
|
213
|
+
return builderLayer(app.events, { layer: runs }, source.#authorizer);
|
|
214
|
+
}
|
|
215
|
+
/** The event types of `app` with what runs them, behind its authorizer. */
|
|
216
|
+
static feedsOf(app) {
|
|
217
|
+
const source = EventImplementation.own(app);
|
|
218
|
+
return Effect.gen(function* () {
|
|
219
|
+
const before = app.events.some((event) => event.caller !== Anyone) ? yield* source.#authorizer.key : () => Effect.void;
|
|
220
|
+
return yield* Effect.forEach(EventImplementation.held(app), ([event, run]) => Effect.map(Effect.service(run.key), (ran) => guarded(event, ran, before)));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/** What runs each event type `app` holds. */
|
|
224
|
+
static held(app) {
|
|
225
|
+
return EventImplementation.own(app).#running.filter(([event]) => app.events.includes(event));
|
|
226
|
+
}
|
|
227
|
+
static own(app) {
|
|
228
|
+
if (!(#running in app)) throw notMadeHere("Event");
|
|
229
|
+
return app;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* Why `surface` cannot serve `feeds` unless its authentication is `keyed` by a principal: a
|
|
234
|
+
* protected event type whose `subscription` hook is held per subscriber between polls, which a
|
|
235
|
+
* principal names.
|
|
236
|
+
*/
|
|
237
|
+
const unkeyedHook = (surface, feeds, keyed) => keyed ? void 0 : Array.findFirst(feeds, ({ event, release }) => release !== void 0 && event.caller !== Anyone).pipe(Option.map(({ event }) => `${surface} event "${event.name}" is protected and has a subscription hook, so its authentication needs principal to tell poll subscribers apart (give Authentication.make a principal, such as (user) => user.id)`), Option.getOrUndefined);
|
|
238
|
+
/** The event implementations among what a surface is given. */
|
|
239
|
+
const eventsOf = (served) => Array.ensure(served).filter((app) => Predicate.isTagged(app, "EventImplementation"));
|
|
240
|
+
/**
|
|
241
|
+
* The event implementations among `apps` holding an event type of `listed`, each holding those
|
|
242
|
+
* alone, refusing one listed that none holds; every one where `listed` is absent.
|
|
243
|
+
*/
|
|
244
|
+
const selectEvents = (apps, listed) => selectHeld(apps, listed, (app) => app.events, (own, app) => EventImplementation.share(own, app));
|
|
245
|
+
/** The builder layers of event implementations, as a surface provides them. */
|
|
246
|
+
const feedBuilders = (events) => events.map((app) => EventImplementation.layerOf(app));
|
|
247
|
+
/** Every feed of event implementations, from the context their builders provide. */
|
|
248
|
+
const acquireFeeds = (events) => Effect.map(Effect.forEach(events, (app) => EventImplementation.feedsOf(app)), (feeds) => feeds.flat());
|
|
249
|
+
export { EventImplementation, acquireFeeds, defaultNextPoll, eventsOf, feedBuilders, selectEvents, sourcesOf, unkeyedHook };
|
package/dist/contract/hints.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/contract/hints.ts
|
|
2
2
|
/**
|
|
3
3
|
* An action's MCP tool hints, MCP's defaults where its `mcp` leaves one out, except that a read
|
|
4
4
|
* is not destructive where MCP's default would say it is: no tool idempotent and every tool
|
|
@@ -11,5 +11,4 @@ const hintsOf = ({ readOnly, mcp }) => ({
|
|
|
11
11
|
idempotentHint: mcp.idempotentHint ?? false,
|
|
12
12
|
openWorldHint: mcp.openWorldHint ?? true
|
|
13
13
|
});
|
|
14
|
-
//#endregion
|
|
15
14
|
export { hintsOf };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { LogStorage } from "./log.mjs";
|
|
2
|
+
import { Any, Emitted } from "../Event.mjs";
|
|
3
|
+
import { Context, Effect, Layer, Scope, Stream } from "effect";
|
|
4
|
+
/** What `notify` and `publish` take: the key whose subscribers they reach. */
|
|
5
|
+
export interface KeyOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Reach only the subscribers whose source's `key` answers this, such as one room's; without
|
|
8
|
+
* it, every subscriber of the event. A subscriber of a source without `key` is reached either way.
|
|
9
|
+
*/
|
|
10
|
+
readonly key?: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
type ErasedEmitted = Emitted<Any>;
|
|
13
|
+
/**
|
|
14
|
+
* Where an `Events` keeps its current sources' revisions: counters by name, which only count up,
|
|
15
|
+
* in an `epoch` telling them from counters that started again.
|
|
16
|
+
*/
|
|
17
|
+
export interface RevisionStorage {
|
|
18
|
+
/** Changes whenever the counters start again, so a cursor from before is behind every revision. */
|
|
19
|
+
readonly epoch: string;
|
|
20
|
+
readonly get: (name: string) => Effect.Effect<number>;
|
|
21
|
+
/** Adds one to each counter `names` names. */
|
|
22
|
+
readonly bump: (names: readonly string[]) => Effect.Effect<void>;
|
|
23
|
+
}
|
|
24
|
+
declare const Events_base: Context.ServiceClass<Events, "effect-actions/Event/Events", {
|
|
25
|
+
/** Wakes the subscribers of `event`'s check source to read it now, rather than at their next poll. */
|
|
26
|
+
readonly notify: (event: Any, options?: KeyOptions) => Effect.Effect<void>;
|
|
27
|
+
/** Appends `emitted` to `event`'s log, delivering it to the log source's subscribers now. */
|
|
28
|
+
readonly publish: <E extends Any>(event: E, emitted: Emitted<E>, options?: KeyOptions) => Effect.Effect<void>;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Where an event's occurrences are announced as they happen: `notify` wakes the subscribers of a
|
|
32
|
+
* check source to read now, and `publish` appends an occurrence to a log source, both reaching
|
|
33
|
+
* the subscribers of one `key` or every one. Domain code yields it as it yields a store, and an
|
|
34
|
+
* entrypoint provides one implementation: `Event.layerMemory`, kept in a process's memory.
|
|
35
|
+
* Every surface serving events and `Action.client` watching them need it.
|
|
36
|
+
*
|
|
37
|
+
* **Gotchas**
|
|
38
|
+
*
|
|
39
|
+
* - The handlers announcing and the surfaces serving must share one, as they share a store: built separately, as a test building its handlers under a `provide` of their own does, each has its own, and an announcement reaches no subscriber of the other.
|
|
40
|
+
* - An event no implementation attached here has no subscribers here: `publish` does nothing for it, and `notify` only moves its revision, so a current source attached later answers the change.
|
|
41
|
+
* - `notify` of a log source and `publish` to a check source die naming the other: a check source reads your store, so write there and notify; a log source keeps what you publish.
|
|
42
|
+
* - Implementations attach only to one `layerMemory` or `makeEvents` made: built over another, such as `Events.of` in a test, they die, naming both.
|
|
43
|
+
*/
|
|
44
|
+
export declare class Events extends Events_base {}
|
|
45
|
+
/** `makeEvents`' options: where it keeps logs, and whether check sources are read on a timer. */
|
|
46
|
+
export interface MakeEventsOptions {
|
|
47
|
+
/**
|
|
48
|
+
* The storage of `event`'s log, made at its first attachment: the core keeps the log's cursors,
|
|
49
|
+
* bounds and expiry over it.
|
|
50
|
+
*/
|
|
51
|
+
readonly log: (event: Any) => LogStorage<ErasedEmitted>;
|
|
52
|
+
/**
|
|
53
|
+
* Whether a check source's subscribers also read it again on a timer, each page's `nextPoll`,
|
|
54
|
+
* besides when notified; defaults to `true`. `false` where a pending timer would keep the host
|
|
55
|
+
* awake, as it keeps a Durable Object from hibernating: its sources are read when notified, and
|
|
56
|
+
* 5 seconds after a read that fails.
|
|
57
|
+
*/
|
|
58
|
+
readonly polls?: boolean | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Whether a log source's `emit` is consumed; defaults to `true`. `false` where the host does not
|
|
61
|
+
* last, as a Durable Object is evicted while idle and misses what a stream sends meanwhile: a
|
|
62
|
+
* log source fed by `emit` dies as it attaches, naming `publish`.
|
|
63
|
+
*/
|
|
64
|
+
readonly emits?: boolean | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Where current sources' revisions are kept; defaults to memory, a new epoch with each
|
|
67
|
+
* `makeEvents`. A host keeping its logs in storage keeps them there too, so a subscriber's
|
|
68
|
+
* cursor outlives the host.
|
|
69
|
+
*/
|
|
70
|
+
readonly revisions?: RevisionStorage | undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* An `Events` for as long as the scope lasts, over the log storage `log` makes: what a host keeping
|
|
74
|
+
* logs elsewhere, such as a Durable Object's storage, provides as `Events`, its subscribers' wakes
|
|
75
|
+
* in memory and its logs where `log` keeps them.
|
|
76
|
+
*
|
|
77
|
+
* **Gotchas**
|
|
78
|
+
*
|
|
79
|
+
* - Make it once per host, around the handlers that announce and the surfaces that serve, as `layerMemory` is built.
|
|
80
|
+
* - A log's storage is made once per event for the life of the scope, its position read on every append and read: a storage emptied behind its back, as `storage.deleteAll` empties one, starts a new epoch, and a cursor from before reads a gap.
|
|
81
|
+
*/
|
|
82
|
+
export declare const makeEvents: (options: MakeEventsOptions) => Effect.Effect<Events["Service"], never, Scope.Scope>;
|
|
83
|
+
/**
|
|
84
|
+
* An `Events` kept in the memory of the process that builds it: for a server holding its
|
|
85
|
+
* subscribers' connections itself, such as Node's, and for tests.
|
|
86
|
+
*
|
|
87
|
+
* **Gotchas**
|
|
88
|
+
*
|
|
89
|
+
* - Build it once, around the handlers that announce and the surfaces that serve, as a store is: each build is its own.
|
|
90
|
+
* - A log lives as long as this build, through rebuilds of the implementations attached to it: a restart forgets it, and a subscriber resuming from one of its cursors receives a gap.
|
|
91
|
+
* - Several builds of one log source share its log, and its `emit` feeds it from one build at a time, so nothing is appended twice.
|
|
92
|
+
*/
|
|
93
|
+
export declare const layerMemory: Layer.Layer<Events>;
|
|
94
|
+
export {};
|