@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,10 +1,23 @@
|
|
|
1
|
-
import { Anyone,
|
|
1
|
+
import { Anyone, CallerKeyed } from "./caller.mjs";
|
|
2
2
|
import { Unauthenticated } from "./errors.mjs";
|
|
3
|
-
import { assertDistinct, assertOnce, heldOnce } from "./names.mjs";
|
|
4
3
|
import { projectedErrors } from "./declared.mjs";
|
|
5
|
-
import {
|
|
6
|
-
|
|
4
|
+
import { checkInput } from "./input-check.mjs";
|
|
5
|
+
import { assertOnce, heldOnce } from "./names.mjs";
|
|
6
|
+
import { reportedTo, streamReportedTo } from "./progress.mjs";
|
|
7
|
+
import { elementOf } from "./stream.mjs";
|
|
8
|
+
import { Array, Cause, Context, Effect, Exit, Layer, Option, Predicate, References, Schema, Stream } from "effect";
|
|
9
|
+
// src/contract/implementation.ts
|
|
7
10
|
const uniqueKey = () => Math.random().toString(36).slice(2);
|
|
11
|
+
const notAStream = /* @__PURE__ */ new Error("A handler answered otherwise than its action's success declares");
|
|
12
|
+
/** The stream of a call of an action answering one, from its `ErasedHandler`'s answer. */
|
|
13
|
+
const elementsOf = (answer) => Stream.unwrap(Effect.filterOrElse(answer, Stream.isStream, () => Effect.die(notAStream)));
|
|
14
|
+
/** The answer of a handler of an action answering one value, a stream refused as a defect. */
|
|
15
|
+
const answered = (answer) => Stream.isStream(answer) ? Effect.die(notAStream) : answer;
|
|
16
|
+
/**
|
|
17
|
+
* `run` as a tool calls it, a tool answering one result: the elements of an action answering a
|
|
18
|
+
* stream collected, in order, into one array once the stream ends.
|
|
19
|
+
*/
|
|
20
|
+
const answeringWhole = (action, run) => elementOf(action.success) === void 0 ? run : (input, reporting) => Stream.runCollect(elementsOf(run(input, reporting)));
|
|
8
21
|
const memoized = (build) => {
|
|
9
22
|
const key = Context.Service(`effect-actions/Implementation/${uniqueKey()}`);
|
|
10
23
|
return {
|
|
@@ -13,6 +26,48 @@ const memoized = (build) => {
|
|
|
13
26
|
};
|
|
14
27
|
};
|
|
15
28
|
/**
|
|
29
|
+
* The builders of an implementation of `contracts` as one layer: what its builder made, and its
|
|
30
|
+
* authorizer where a contract is protected, as only those need one.
|
|
31
|
+
*/
|
|
32
|
+
const builderLayer = (contracts, built, authorizer) => contracts.some((contract) => contract.caller !== Anyone) ? Layer.merge(built.layer, authorizer.layer) : built.layer;
|
|
33
|
+
/** What a surface throws for an implementation some other copy of this module made. */
|
|
34
|
+
const notMadeHere = (module) => /* @__PURE__ */ new Error(`Not an implementation made by this ${module}.implement: pass the value implement returned, not a copy such as a spread; or effect-actions is installed twice`);
|
|
35
|
+
const asDefects = (reason, rejected) => {
|
|
36
|
+
const trace = Context.makeUnsafe(reason.annotations);
|
|
37
|
+
return [Cause.makeDieReason(reason.error).annotate(trace), Cause.makeDieReason(Cause.squash(rejected)).annotate(trace)];
|
|
38
|
+
};
|
|
39
|
+
const dispatch = (action, handle, before, keyed) => {
|
|
40
|
+
const actionAttributes = {
|
|
41
|
+
"action.name": action.name,
|
|
42
|
+
"action.read_only": action.readOnly
|
|
43
|
+
};
|
|
44
|
+
const authentication = action.caller === Anyone ? Effect.void : Effect.flatMap(Effect.serviceOption(action.caller), (actor) => Option.isSome(actor) ? Effect.void : Effect.fail(new Unauthenticated()));
|
|
45
|
+
const authorization = action.caller === Anyone ? Effect.void : Effect.suspend(() => before(action));
|
|
46
|
+
const validate = Schema.decodeUnknownEffect(Schema.toType(Schema.Union(projectedErrors(action))));
|
|
47
|
+
const declaredOrDefect = Effect.fnUntraced(function* (cause) {
|
|
48
|
+
const reasons = [];
|
|
49
|
+
for (const reason of cause.reasons) if (Cause.isFailReason(reason)) {
|
|
50
|
+
const validated = yield* Effect.exit(validate(reason.error));
|
|
51
|
+
reasons.push(...Exit.isSuccess(validated) ? [reason] : asDefects(reason, validated.cause));
|
|
52
|
+
} else reasons.push(reason);
|
|
53
|
+
return yield* Effect.failCause(Cause.fromReasons(reasons));
|
|
54
|
+
});
|
|
55
|
+
const admitted = (input) => Effect.andThen(authentication, Effect.andThen(checkInput(action, input), authorization));
|
|
56
|
+
const span = { attributes: actionAttributes };
|
|
57
|
+
const trace = { captureStackTrace: false };
|
|
58
|
+
if (elementOf(action.success) !== void 0) {
|
|
59
|
+
const streamed = (answer) => Stream.isStream(answer) ? Stream.scoped(answer) : Stream.die(notAStream);
|
|
60
|
+
return (input, sink) => Effect.succeed(Stream.unwrap(Effect.map(admitted(input), () => streamReportedTo(action.progress, sink, (reporting) => streamed(handle(input, reporting))))).pipe(Stream.catchCause((cause) => Stream.fromEffect(declaredOrDefect(cause))), Stream.updateService(References.CurrentLogAnnotations, (annotations) => ({
|
|
61
|
+
...annotations,
|
|
62
|
+
...actionAttributes
|
|
63
|
+
})), Stream.withSpan(action.name, {
|
|
64
|
+
...span,
|
|
65
|
+
...trace
|
|
66
|
+
})));
|
|
67
|
+
}
|
|
68
|
+
return (input, sink) => Effect.scoped(admitted(input).pipe(Effect.andThen(Effect.suspend(() => reportedTo(action.progress, sink, (reporting) => answered(handle(input, reporting))))), Effect.provideService(CallerKeyed, keyed), Effect.catchCause(declaredOrDefect))).pipe(Effect.annotateLogs(actionAttributes), Effect.withSpan(action.name, span, trace));
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
16
71
|
* Actions bound to their handlers and their authorizer: everything one `Action.implement` call
|
|
17
72
|
* produced, one builder serving every surface. The private fields make this class nominal: a structurally similar object, including one made by
|
|
18
73
|
* spreading an implementation, is not an implementation. Its type parameters may be written
|
|
@@ -22,6 +77,7 @@ const memoized = (build) => {
|
|
|
22
77
|
* failures and services of the authorizer's builder.
|
|
23
78
|
*/
|
|
24
79
|
var Implementation = class Implementation {
|
|
80
|
+
/** The contracts this implementation answers. */
|
|
25
81
|
actions;
|
|
26
82
|
#handlers;
|
|
27
83
|
#authorizer;
|
|
@@ -49,28 +105,32 @@ var Implementation = class Implementation {
|
|
|
49
105
|
*/
|
|
50
106
|
static layerOf(app) {
|
|
51
107
|
const source = Implementation.own(app);
|
|
52
|
-
return app.actions
|
|
108
|
+
return builderLayer(app.actions, source.#handlers, source.#authorizer);
|
|
53
109
|
}
|
|
54
110
|
/**
|
|
55
111
|
* The actions of `app` with their handlers, behind its authorizer, from the context
|
|
56
112
|
* `layerOf(app)` provides.
|
|
57
113
|
*/
|
|
58
|
-
static boundOf(app) {
|
|
114
|
+
static boundOf(app, keyed) {
|
|
59
115
|
const { actions } = app;
|
|
60
116
|
const source = Implementation.own(app);
|
|
61
117
|
return Effect.gen(function* () {
|
|
62
118
|
const bound = yield* source.#handlers.key;
|
|
63
119
|
const before = actions.some((action) => action.caller !== Anyone) ? yield* source.#authorizer.key : () => Effect.void;
|
|
64
|
-
return actions.flatMap((action) => bound.flatMap(([held, handle]) => held === action ? [[action, dispatch(action, handle, before)]] : []));
|
|
120
|
+
return actions.flatMap((action) => bound.flatMap(([held, handle]) => held === action ? [[action, dispatch(action, handle, before, keyed)]] : []));
|
|
65
121
|
});
|
|
66
122
|
}
|
|
67
123
|
/** `app`, if this copy of the module made it; another installed copy's cannot be read. */
|
|
68
124
|
static own(app) {
|
|
69
|
-
if (!(#handlers in app)) throw
|
|
125
|
+
if (!(#handlers in app)) throw notMadeHere("Action");
|
|
70
126
|
return app;
|
|
71
127
|
}
|
|
72
128
|
};
|
|
73
|
-
|
|
129
|
+
/**
|
|
130
|
+
* The action implementations among what a surface is given; `eventsOf` reads the event
|
|
131
|
+
* implementations, by their tag.
|
|
132
|
+
*/
|
|
133
|
+
const toList = (served) => Array.ensure(served).filter((app) => !Predicate.isTagged(app, "EventImplementation"));
|
|
74
134
|
const notHeld = {
|
|
75
135
|
byImplementation: {
|
|
76
136
|
why: "no implementation holds it",
|
|
@@ -93,15 +153,22 @@ const assertHeld = ({ why, fix, holder }, listed, held) => {
|
|
|
93
153
|
throw new Error(`Listed in actions, but ${why}: ${markingOthers(missing, names)} (${fix}${sameName ? `; one marked (another contract) is a contract of that name ${holder}` : ""})`);
|
|
94
154
|
}
|
|
95
155
|
};
|
|
96
|
-
|
|
156
|
+
/**
|
|
157
|
+
* The implementations among `apps` holding what `listed` lists, each sharing its builder to hold
|
|
158
|
+
* those alone, or every one where it lists none: action and event implementations alike, by the
|
|
159
|
+
* contracts each `holds` and how it `share`s them.
|
|
160
|
+
*/
|
|
161
|
+
const selectHeld = (apps, listed, holds, share) => {
|
|
97
162
|
if (listed === void 0) return apps;
|
|
98
|
-
assertHeld(notHeld.byImplementation, listed, apps.flatMap(
|
|
163
|
+
assertHeld(notHeld.byImplementation, listed, apps.flatMap(holds));
|
|
99
164
|
return apps.flatMap((app) => {
|
|
100
|
-
const
|
|
165
|
+
const held = holds(app);
|
|
166
|
+
const own = held.filter((contract) => listed.includes(contract));
|
|
101
167
|
if (own.length === 0) return [];
|
|
102
|
-
return own.length ===
|
|
168
|
+
return own.length === held.length ? [app] : [share(own, app)];
|
|
103
169
|
});
|
|
104
170
|
};
|
|
171
|
+
const select = (apps, listed) => selectHeld(apps, listed, (app) => app.actions, (own, app) => Implementation.share(own, app));
|
|
105
172
|
const servedBy = (what, bound, apps, listed) => {
|
|
106
173
|
if (listed !== void 0) assertHeld(notHeld.byBinding, listed, bound);
|
|
107
174
|
const held = apps.flatMap((app) => app.actions);
|
|
@@ -114,59 +181,26 @@ const servedBy = (what, bound, apps, listed) => {
|
|
|
114
181
|
assertOnce("served action", actions, heldOnce);
|
|
115
182
|
return served;
|
|
116
183
|
};
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
*/
|
|
121
|
-
const servingOf = (what, binding, served, options) => {
|
|
122
|
-
const apps = servedBy(what, binding.actions, toList(served), options.actions);
|
|
123
|
-
const actions = apps.flatMap((app) => app.actions);
|
|
124
|
-
assertAuthentication(actions, binding.authentication);
|
|
125
|
-
assertDistinct("middleware", options.middleware ?? [], (key) => key.key, { fix: "list each middleware once" });
|
|
126
|
-
return {
|
|
127
|
-
apps,
|
|
128
|
-
actions,
|
|
129
|
-
authentication: actions.some((action) => action.caller !== Anyone) ? binding.authentication : void 0
|
|
130
|
-
};
|
|
131
|
-
};
|
|
132
|
-
const provideHandlers = (apps) => (layer) => {
|
|
133
|
-
const [first, ...rest] = apps.map((app) => Implementation.layerOf(app));
|
|
184
|
+
/** Provide `builders`, implementations' builder layers, to a surface's layer. */
|
|
185
|
+
const provideBuilders = (builders) => (layer) => {
|
|
186
|
+
const [first, ...rest] = builders;
|
|
134
187
|
return first === void 0 ? layer : Layer.provide(layer, Layer.mergeAll(first, ...rest));
|
|
135
188
|
};
|
|
136
|
-
const
|
|
137
|
-
const acquire = (apps) => Effect.map(Effect.forEach(apps, (app) => Implementation.boundOf(app)), (bound) => bound.flat());
|
|
138
|
-
|
|
139
|
-
|
|
189
|
+
const provideHandlers = (apps) => provideBuilders(apps.map((app) => Implementation.layerOf(app)));
|
|
190
|
+
const acquire = (apps, keyed) => Effect.map(Effect.forEach(apps, (app) => Implementation.boundOf(app, keyed)), (bound) => bound.flat());
|
|
191
|
+
/**
|
|
192
|
+
* What `acquisition` reads, once `builders` are built into the layer graph around the caller, and
|
|
193
|
+
* held by its scope.
|
|
194
|
+
*/
|
|
195
|
+
const builtWith = (builders, acquisition) => {
|
|
196
|
+
const [first, ...rest] = builders;
|
|
140
197
|
return Effect.gen(function* () {
|
|
141
|
-
if (first === void 0) return
|
|
198
|
+
if (first === void 0) return yield* acquisition;
|
|
142
199
|
const current = yield* Effect.serviceOption(Layer.CurrentMemoMap);
|
|
143
200
|
const memoMap = Option.getOrElse(current, Layer.makeMemoMapUnsafe);
|
|
144
201
|
const scope = yield* Effect.scope;
|
|
145
202
|
const context = yield* Layer.buildWithMemoMap(Layer.mergeAll(first, ...rest), memoMap, scope);
|
|
146
|
-
return yield* Effect.provideContext(
|
|
147
|
-
});
|
|
148
|
-
};
|
|
149
|
-
const dispatch = (action, handle, before) => {
|
|
150
|
-
const actionAttributes = {
|
|
151
|
-
"action.name": action.name,
|
|
152
|
-
"action.read_only": action.readOnly
|
|
153
|
-
};
|
|
154
|
-
const authentication = action.caller === Anyone ? Effect.void : Effect.flatMap(Effect.serviceOption(action.caller), (actor) => Option.isSome(actor) ? Effect.void : Effect.fail(new Unauthenticated()));
|
|
155
|
-
const authorization = action.caller === Anyone ? Effect.void : Effect.suspend(() => before(action));
|
|
156
|
-
const validate = Schema.decodeUnknownEffect(Schema.toType(Schema.Union(projectedErrors(action))));
|
|
157
|
-
const asDefects = (reason, rejected) => {
|
|
158
|
-
const trace = Context.makeUnsafe(reason.annotations);
|
|
159
|
-
return [Cause.makeDieReason(reason.error).annotate(trace), Cause.makeDieReason(Cause.squash(rejected)).annotate(trace)];
|
|
160
|
-
};
|
|
161
|
-
const declaredOrDefect = Effect.fnUntraced(function* (cause) {
|
|
162
|
-
const reasons = [];
|
|
163
|
-
for (const reason of cause.reasons) if (Cause.isFailReason(reason)) {
|
|
164
|
-
const validated = yield* Effect.exit(validate(reason.error));
|
|
165
|
-
reasons.push(...Exit.isSuccess(validated) ? [reason] : asDefects(reason, validated.cause));
|
|
166
|
-
} else reasons.push(reason);
|
|
167
|
-
return yield* Effect.failCause(Cause.fromReasons(reasons));
|
|
203
|
+
return yield* Effect.provideContext(acquisition, context);
|
|
168
204
|
});
|
|
169
|
-
return (input) => Effect.scoped(authentication.pipe(Effect.andThen(authorization), Effect.andThen(Effect.suspend(() => handle(input))), Effect.catchCause(declaredOrDefect))).pipe(Effect.annotateLogs(actionAttributes), Effect.withSpan(action.name, { attributes: actionAttributes }, { captureStackTrace: false }));
|
|
170
205
|
};
|
|
171
|
-
|
|
172
|
-
export { Implementation, acquire, assertHeld, builders, built, memoized, notHeld, provideHandlers, select, servingOf, toList };
|
|
206
|
+
export { Implementation, acquire, answered, answeringWhole, assertHeld, builderLayer, builtWith, elementsOf, markingOthers, memoized, notHeld, notMadeHere, provideBuilders, provideHandlers, select, selectHeld, servedBy, toList, uniqueKey };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Forbidden } from "./errors.mjs";
|
|
2
|
+
import { Any } from "../Event.mjs";
|
|
3
|
+
import { Any as Any$1 } from "../Action.mjs";
|
|
4
|
+
import { Context, Effect } from "effect";
|
|
5
|
+
/**
|
|
6
|
+
* A check every call and every subscription runs on its decoded input, on every surface, once the
|
|
7
|
+
* surface has verified its caller and before its authorizer: what a host provides, as a Durable
|
|
8
|
+
* Object refuses an input naming another object, to refuse an input it does not serve. Defaults
|
|
9
|
+
* to none.
|
|
10
|
+
*
|
|
11
|
+
* **Gotchas**
|
|
12
|
+
*
|
|
13
|
+
* - Provide it to the layer graph the surfaces are built in, as `Layer.succeed(Action.InputCheck)(check)`: provided per request, it misses a socket's later calls.
|
|
14
|
+
*/
|
|
15
|
+
export declare const InputCheck: Context.Reference<(contract: Any$1 | Any, input: Any$1["input"]["Type"]) => Effect.Effect<void, Forbidden>>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Context, Effect } from "effect";
|
|
2
|
+
// src/contract/input-check.ts
|
|
3
|
+
/**
|
|
4
|
+
* A check every call and every subscription runs on its decoded input, on every surface, once the
|
|
5
|
+
* surface has verified its caller and before its authorizer: what a host provides, as a Durable
|
|
6
|
+
* Object refuses an input naming another object, to refuse an input it does not serve. Defaults
|
|
7
|
+
* to none.
|
|
8
|
+
*
|
|
9
|
+
* **Gotchas**
|
|
10
|
+
*
|
|
11
|
+
* - Provide it to the layer graph the surfaces are built in, as `Layer.succeed(Action.InputCheck)(check)`: provided per request, it misses a socket's later calls.
|
|
12
|
+
*/
|
|
13
|
+
const InputCheck = Context.Reference("effect-actions/Action/InputCheck", { defaultValue: () => () => Effect.void });
|
|
14
|
+
/** `contract`'s `input`, as the provided `InputCheck` judges it. */
|
|
15
|
+
const checkInput = (contract, input) => Effect.flatMap(Effect.service(InputCheck), (check) => check(contract, input));
|
|
16
|
+
export { InputCheck, checkInput };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Context, Duration, Effect, Equal, Hash, RcMap, Scope } from "effect";
|
|
2
|
+
// src/contract/lease.ts
|
|
3
|
+
/** A poll subscription's key, held with the context of the poll that first acquired it. */
|
|
4
|
+
var Lease = class Lease {
|
|
5
|
+
key;
|
|
6
|
+
input;
|
|
7
|
+
context;
|
|
8
|
+
constructor(key, input, context) {
|
|
9
|
+
this.key = key;
|
|
10
|
+
this.input = input;
|
|
11
|
+
this.context = context;
|
|
12
|
+
}
|
|
13
|
+
[Equal.symbol](that) {
|
|
14
|
+
return that instanceof Lease && that.key === this.key;
|
|
15
|
+
}
|
|
16
|
+
[Hash.symbol]() {
|
|
17
|
+
return Hash.string(this.key);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const defaultLease = Duration.minutes(5);
|
|
21
|
+
const makeLeases = (subscription, ttl = defaultLease) => Effect.map(RcMap.make({
|
|
22
|
+
lookup: (lease) => Effect.provideContext(subscription(lease.input), Context.omit(Scope.Scope)(lease.context)),
|
|
23
|
+
idleTimeToLive: ttl
|
|
24
|
+
}), (map) => ({
|
|
25
|
+
hold: (key, input) => Effect.flatMap(Effect.context(), (context) => Effect.scoped(RcMap.get(map, new Lease(key, input, context)))),
|
|
26
|
+
release: (key) => RcMap.invalidate(map, new Lease(key, void 0, Context.empty()))
|
|
27
|
+
}));
|
|
28
|
+
export { makeLeases };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import "../Event.mjs";
|
|
2
|
+
import "../Action.mjs";
|
|
3
|
+
/** The members of the list `T` that are `U`: `T` itself where every one is, `[]` where none is. */
|
|
4
|
+
export type Only<T extends readonly unknown[], U> = [Exclude<T[number], U>] extends [never] ? Extract<T, readonly U[]> : [Extract<T[number], U>] extends [never] ? readonly [] : ReadonlyArray<Extract<T[number], U>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { assertOnce } from "./names.mjs";
|
|
2
|
+
import { isEvent } from "../Event.mjs";
|
|
3
|
+
// src/contract/listed.ts
|
|
4
|
+
/** A list split into its actions and its event types. */
|
|
5
|
+
const split = (contracts) => ({
|
|
6
|
+
actions: contracts.flatMap((contract) => isEvent(contract) ? [] : [contract]),
|
|
7
|
+
events: contracts.filter(isEvent)
|
|
8
|
+
});
|
|
9
|
+
/** A binding's list split into its actions and its event types, each name used once. */
|
|
10
|
+
const listed = (contracts) => {
|
|
11
|
+
assertOnce("action or event", contracts);
|
|
12
|
+
return split(contracts);
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A surface's `actions` option split into the actions and the event types it lists, both absent
|
|
16
|
+
* where it is: a surface given the option serves exactly what it lists.
|
|
17
|
+
*/
|
|
18
|
+
const selection = (contracts) => contracts === void 0 ? {
|
|
19
|
+
actions: void 0,
|
|
20
|
+
events: void 0
|
|
21
|
+
} : split(contracts);
|
|
22
|
+
export { listed, selection, split };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Duration, Effect, Stream } from "effect";
|
|
2
|
+
/** One entry of a log: what was published, and the key it was published to, if any. */
|
|
3
|
+
export interface Logged<A> {
|
|
4
|
+
readonly value: A;
|
|
5
|
+
readonly key: string | undefined;
|
|
6
|
+
}
|
|
7
|
+
/** Where a log stands: the epoch its cursors name, and the last sequence number it gave. */
|
|
8
|
+
export interface Position {
|
|
9
|
+
readonly epoch: string;
|
|
10
|
+
readonly last: number;
|
|
11
|
+
}
|
|
12
|
+
/** One entry as a log's storage keeps it: when it was appended, and what was published. */
|
|
13
|
+
export interface Stored<A> {
|
|
14
|
+
readonly at: number;
|
|
15
|
+
readonly logged: Logged<A>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One entry as a log's storage lists it: as it was kept, or with `logged` `undefined` where the
|
|
19
|
+
* storage could not read it back, such as data an earlier deployment wrote in another shape,
|
|
20
|
+
* which a subscriber receives as a gap at its cursor.
|
|
21
|
+
*/
|
|
22
|
+
export interface Listed<A> {
|
|
23
|
+
readonly at: number;
|
|
24
|
+
readonly logged: Logged<A> | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Where a log keeps its position and its entries, by sequence number, as `Event.makeEvents` is
|
|
28
|
+
* given it: storage alone, the log's cursors, bounds, truncation and expiry being the core's.
|
|
29
|
+
*/
|
|
30
|
+
export interface LogStorage<A> {
|
|
31
|
+
/** The position `put` last kept, or `undefined` for a storage never written or emptied. */
|
|
32
|
+
readonly position: Effect.Effect<Position | undefined>;
|
|
33
|
+
/** Keeps `position`, and `entry` with it where given, at once. */
|
|
34
|
+
readonly put: (position: Position, entry?: {
|
|
35
|
+
readonly seq: number;
|
|
36
|
+
readonly stored: Stored<A>;
|
|
37
|
+
}) => Effect.Effect<void>;
|
|
38
|
+
/** The entries kept from sequence number `from` on, at most `limit`, in order. */
|
|
39
|
+
readonly list: (from: number, limit: number) => Effect.Effect<ReadonlyArray<readonly [seq: number, listed: Listed<A>]>>;
|
|
40
|
+
readonly delete: (seqs: readonly number[]) => Effect.Effect<void>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { uniqueKey } from "./implementation.mjs";
|
|
2
|
+
import { reported } from "./reported.mjs";
|
|
3
|
+
import { Cause, Clock, Duration, Effect, Option, Queue, Semaphore, Stream } from "effect";
|
|
4
|
+
// src/contract/log.ts
|
|
5
|
+
const defaultBounds = {
|
|
6
|
+
maxAge: Duration.minutes(10),
|
|
7
|
+
maxEvents: 1e4
|
|
8
|
+
};
|
|
9
|
+
/** A log storage in memory, for the process that makes it. */
|
|
10
|
+
const memoryStorage = () => {
|
|
11
|
+
let position;
|
|
12
|
+
let newest = 0;
|
|
13
|
+
const entries = /* @__PURE__ */ new Map();
|
|
14
|
+
return {
|
|
15
|
+
position: Effect.sync(() => position),
|
|
16
|
+
put: (next, entry) => Effect.sync(() => {
|
|
17
|
+
position = next;
|
|
18
|
+
if (entry === void 0) return;
|
|
19
|
+
entries.set(entry.seq, entry.stored);
|
|
20
|
+
newest = Math.max(newest, entry.seq);
|
|
21
|
+
}),
|
|
22
|
+
list: (from, limit) => Effect.sync(() => {
|
|
23
|
+
const listed = [];
|
|
24
|
+
const oldest = entries.keys().next().value;
|
|
25
|
+
if (oldest === void 0) return listed;
|
|
26
|
+
for (let seq = Math.max(from, oldest); seq <= newest && listed.length < limit; seq += 1) {
|
|
27
|
+
const stored = entries.get(seq);
|
|
28
|
+
if (stored !== void 0) listed.push([seq, stored]);
|
|
29
|
+
}
|
|
30
|
+
return listed;
|
|
31
|
+
}),
|
|
32
|
+
delete: (seqs) => Effect.sync(() => {
|
|
33
|
+
for (const seq of seqs) entries.delete(seq);
|
|
34
|
+
})
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const separator = ":";
|
|
38
|
+
const cursorAt = (epoch, seq) => `${epoch}${separator}${seq}`;
|
|
39
|
+
const seqIn = (epoch, cursor) => {
|
|
40
|
+
const [named, seq, ...rest] = cursor.split(separator);
|
|
41
|
+
const parsed = Number(seq);
|
|
42
|
+
return named === epoch && rest.length === 0 && Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A log's entries over `storage`, read by cursor: a cursor is the storage's epoch and the
|
|
46
|
+
* sequence number of the last entry it has seen, so one minted by another epoch, such as a
|
|
47
|
+
* memory log's before a restart or a storage's before it was emptied, reads as truncated, as
|
|
48
|
+
* does one whose next entries were dropped. An epoch is minted where the storage holds none.
|
|
49
|
+
*/
|
|
50
|
+
const makeLogStore = (storage, { maxAge = defaultBounds.maxAge, maxEvents = defaultBounds.maxEvents } = {}) => {
|
|
51
|
+
if (!Number.isSafeInteger(maxEvents) || maxEvents < 1) throw new Error(`Invalid log maxEvents: ${maxEvents} (give a positive integer)`);
|
|
52
|
+
const age = Duration.toMillis(Duration.fromInputUnsafe(maxAge));
|
|
53
|
+
const turn = Semaphore.makeUnsafe(1);
|
|
54
|
+
const positioned = Effect.flatMap(storage.position, (found) => {
|
|
55
|
+
if (found !== void 0) return Effect.succeed(found);
|
|
56
|
+
const minted = {
|
|
57
|
+
epoch: uniqueKey(),
|
|
58
|
+
last: 0
|
|
59
|
+
};
|
|
60
|
+
return Effect.as(storage.put(minted), minted);
|
|
61
|
+
});
|
|
62
|
+
const oldestKept = (last, now) => Effect.gen(function* () {
|
|
63
|
+
for (let batch = 1;; batch = 64) {
|
|
64
|
+
const listed = yield* storage.list(0, batch);
|
|
65
|
+
const young = listed.findIndex(([, { at }]) => at >= now - age);
|
|
66
|
+
const dropped = young === -1 ? listed : listed.slice(0, young);
|
|
67
|
+
if (dropped.length > 0) yield* storage.delete(dropped.map(([seq]) => seq));
|
|
68
|
+
const first = listed[dropped.length];
|
|
69
|
+
if (first !== void 0) return Math.max(first[0], last - maxEvents + 1);
|
|
70
|
+
if (listed.length < batch) return last + 1;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
append: (logged) => turn.withPermit(Effect.gen(function* () {
|
|
75
|
+
const { epoch, last } = yield* positioned;
|
|
76
|
+
const seq = last + 1;
|
|
77
|
+
const at = yield* Clock.currentTimeMillis;
|
|
78
|
+
yield* storage.put({
|
|
79
|
+
epoch,
|
|
80
|
+
last: seq
|
|
81
|
+
}, {
|
|
82
|
+
seq,
|
|
83
|
+
stored: {
|
|
84
|
+
at,
|
|
85
|
+
logged
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
if (seq > maxEvents) yield* storage.delete([seq - maxEvents]);
|
|
89
|
+
})),
|
|
90
|
+
read: (cursor, limit) => turn.withPermit(Effect.gen(function* () {
|
|
91
|
+
const { epoch, last } = yield* positioned;
|
|
92
|
+
const oldest = yield* oldestKept(last, yield* Clock.currentTimeMillis);
|
|
93
|
+
const seq = cursor === null ? last : seqIn(epoch, cursor);
|
|
94
|
+
const { after, truncated } = seq === void 0 || seq > last || seq + 1 < oldest ? {
|
|
95
|
+
after: oldest - 1,
|
|
96
|
+
truncated: true
|
|
97
|
+
} : {
|
|
98
|
+
after: seq,
|
|
99
|
+
truncated: false
|
|
100
|
+
};
|
|
101
|
+
const pending = yield* storage.list(after + 1, limit + 1);
|
|
102
|
+
const page = pending.slice(0, limit);
|
|
103
|
+
return {
|
|
104
|
+
entries: page.map(([at, stored]) => ({
|
|
105
|
+
seq: at,
|
|
106
|
+
cursor: cursorAt(epoch, at),
|
|
107
|
+
at: stored.at,
|
|
108
|
+
value: stored.logged
|
|
109
|
+
})),
|
|
110
|
+
after: cursorAt(epoch, after),
|
|
111
|
+
cursor: cursorAt(epoch, page.at(-1)?.[0] ?? after),
|
|
112
|
+
truncated,
|
|
113
|
+
hasMore: pending.length > limit
|
|
114
|
+
};
|
|
115
|
+
}))
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
/** A log over `store`, waking its followers as each entry is appended. */
|
|
119
|
+
const makeLog = (store) => {
|
|
120
|
+
const followers = /* @__PURE__ */ new Set();
|
|
121
|
+
const append = (value, key) => Effect.andThen(store.append({
|
|
122
|
+
value,
|
|
123
|
+
key
|
|
124
|
+
}), Effect.sync(() => {
|
|
125
|
+
for (const woken of followers) Queue.offerUnsafe(woken, void 0);
|
|
126
|
+
}));
|
|
127
|
+
const following = Effect.acquireRelease(Effect.tap(Queue.sliding(1), (woken) => Effect.sync(() => followers.add(woken))), (woken) => Effect.sync(() => followers.delete(woken)));
|
|
128
|
+
return {
|
|
129
|
+
append,
|
|
130
|
+
feed: (emit) => emit.pipe(Stream.runForEach(({ value, key }) => append(value, key)), Effect.matchCauseEffect({
|
|
131
|
+
onFailure: (cause) => Cause.hasInterruptsOnly(cause) ? Effect.void : reported(cause),
|
|
132
|
+
onSuccess: () => Effect.logWarning("A log source's emit stream ended: it appends nothing more")
|
|
133
|
+
})),
|
|
134
|
+
read: store.read,
|
|
135
|
+
follow: (cursor, limit) => Stream.unwrap(Effect.map(following, (woken) => Stream.paginate({
|
|
136
|
+
cursor,
|
|
137
|
+
wait: false
|
|
138
|
+
}, (state) => Effect.map(Effect.andThen(state.wait ? Queue.take(woken) : Effect.void, store.read(state.cursor, limit)), (answered) => [[answered], Option.some({
|
|
139
|
+
cursor: answered.cursor,
|
|
140
|
+
wait: !answered.hasMore
|
|
141
|
+
})]))))
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
export { makeLog, makeLogStore, memoryStorage };
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Schema, SchemaAST } from "effect";
|
|
2
|
-
//#region src/contract/media.d.ts
|
|
3
2
|
/**
|
|
4
3
|
* An image, a media field of a success: `{ data: Uint8Array, mimeType: string }`, `mimeType` such
|
|
5
4
|
* as `image/png`. A top-level field of a struct success, one, optional, or a required array, or the
|
|
@@ -13,5 +12,4 @@ import { Schema, SchemaAST } from "effect";
|
|
|
13
12
|
export declare const Image: Schema.Struct<{
|
|
14
13
|
readonly data: Schema.Uint8Array;
|
|
15
14
|
readonly mimeType: Schema.String;
|
|
16
|
-
}>;
|
|
17
|
-
//#endregion
|
|
15
|
+
}>;
|
package/dist/contract/media.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
|
|
2
|
+
// src/contract/media.ts
|
|
3
3
|
const mediaKindAnnotation = "effect-actions/media";
|
|
4
4
|
const Media = Schema.Struct({
|
|
5
5
|
data: Schema.Uint8Array,
|
|
@@ -18,5 +18,4 @@ const Media = Schema.Struct({
|
|
|
18
18
|
const Image = Media.annotate({ [mediaKindAnnotation]: "image" });
|
|
19
19
|
const isMarked = (ast) => ast.annotations?.[mediaKindAnnotation] === "image";
|
|
20
20
|
const kindOf = (ast) => isMarked(ast) && ast.encoding === void 0 ? "image" : void 0;
|
|
21
|
-
//#endregion
|
|
22
21
|
export { Image, Media, isMarked, kindOf };
|
package/dist/contract/names.mjs
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
// src/contract/names.ts
|
|
2
2
|
const namePattern = "[A-Za-z0-9_-]{1,128}";
|
|
3
|
-
const validName = new RegExp(`^${namePattern}
|
|
3
|
+
const validName = new RegExp(`^${namePattern}$`, "u");
|
|
4
|
+
const reserved = [["then", "which would make a client a thenable, awaited as a promise"], ["watch", "every client's method for events"]];
|
|
5
|
+
const reservedNames = reserved.map(([name]) => name);
|
|
6
|
+
/** The reserved names as a message lists them: `"then" or "watch"`. */
|
|
7
|
+
const reservedList = reservedNames.map((name) => `"${name}"`).join(" or ");
|
|
4
8
|
const assertName = (name) => {
|
|
5
|
-
if (!validName.test(name) || name
|
|
9
|
+
if (!validName.test(name) || reservedNames.includes(name)) throw new Error(`Invalid action name: ${name} (use ${namePattern}, other than ${reserved.map(([reservedName, why]) => `"${reservedName}", ${why}`).join(", and ")})`);
|
|
10
|
+
};
|
|
11
|
+
const validEventName = new RegExp(`^[a-z0-9_-]+(\\.[a-z0-9_-]+)*$`, "u");
|
|
12
|
+
const assertEventName = (name) => {
|
|
13
|
+
if (name.length > 128 || !validEventName.test(name)) throw new Error(`Invalid event name: ${name} (use lowercase dot-separated segments of [a-z0-9_-], at most 128 characters)`);
|
|
6
14
|
};
|
|
7
15
|
const assertDistinct = (what, items, nameOf, { claimantOf = nameOf, labelOf = nameOf, fix }) => {
|
|
8
16
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -18,10 +26,11 @@ const assertDistinct = (what, items, nameOf, { claimantOf = nameOf, labelOf = na
|
|
|
18
26
|
}
|
|
19
27
|
};
|
|
20
28
|
const heldOnce = "pass one implementation of it, or rename one";
|
|
21
|
-
const assertOnce = (what, actions, fix = "list it once, or rename one") =>
|
|
29
|
+
const assertOnce = (what, actions, fix = "list it once, or rename one") => {
|
|
30
|
+
assertDistinct(what, actions, (action) => action.name, { fix });
|
|
31
|
+
};
|
|
22
32
|
const assertKnown = (what, keys, names, fix) => {
|
|
23
33
|
const unknown = keys.filter((key) => !names.includes(key));
|
|
24
34
|
if (unknown.length > 0) throw new Error(`Unknown ${what}: ${unknown.join(", ")} (${fix})`);
|
|
25
35
|
};
|
|
26
|
-
|
|
27
|
-
export { assertDistinct, assertKnown, assertName, assertOnce, heldOnce };
|
|
36
|
+
export { assertDistinct, assertEventName, assertKnown, assertName, assertOnce, heldOnce, reservedList, reservedNames };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Any } from "../Event.mjs";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
/**
|
|
4
|
+
* Occurrences were missed: the cursor resumed from was no longer served, or delivery skipped
|
|
5
|
+
* some. `cursor` is where delivery continues.
|
|
6
|
+
*/
|
|
7
|
+
export declare const Gap: Schema.TaggedStruct<"Gap", {
|
|
8
|
+
readonly cursor: Schema.NullOr<Schema.String>;
|
|
9
|
+
}>;
|
|
10
|
+
/** A gap in what a subscriber received. */
|
|
11
|
+
export type Gap = typeof Gap.Type;
|
|
12
|
+
/**
|
|
13
|
+
* Where a feed has read up to without an occurrence to show for it, such as a quiet page or an
|
|
14
|
+
* occurrence a subscriber's `match` rejected: a watch keeps it as its cursor and never yields it.
|
|
15
|
+
*/
|
|
16
|
+
export declare const Checked: Schema.TaggedStruct<"Checked", {
|
|
17
|
+
readonly cursor: Schema.NullOr<Schema.String>;
|
|
18
|
+
}>;
|
|
19
|
+
export type Checked = typeof Checked.Type;
|
|
20
|
+
declare const fieldsOf: <Name extends Schema.Top, Data extends Schema.Top>(name: Name, data: Data) => {
|
|
21
|
+
eventId: Schema.String;
|
|
22
|
+
name: Name;
|
|
23
|
+
timestamp: Schema.DateTimeUtc;
|
|
24
|
+
data: Data;
|
|
25
|
+
cursor: Schema.NullOr<Schema.String>;
|
|
26
|
+
};
|
|
27
|
+
/** The schema of an occurrence of `E`. */
|
|
28
|
+
export type OccurrenceSchema<E extends Any> = Schema.TaggedStruct<"Occurrence", ReturnType<typeof fieldsOf<Schema.Literal<E["name"]>, E["payload"]>>>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Effect, Schema, Struct } from "effect";
|
|
2
|
+
// src/contract/occurrence.ts
|
|
3
|
+
/**
|
|
4
|
+
* Occurrences were missed: the cursor resumed from was no longer served, or delivery skipped
|
|
5
|
+
* some. `cursor` is where delivery continues.
|
|
6
|
+
*/
|
|
7
|
+
const Gap = Schema.TaggedStruct("Gap", { cursor: Schema.NullOr(Schema.String) });
|
|
8
|
+
/**
|
|
9
|
+
* Where a feed has read up to without an occurrence to show for it, such as a quiet page or an
|
|
10
|
+
* occurrence a subscriber's `match` rejected: a watch keeps it as its cursor and never yields it.
|
|
11
|
+
*/
|
|
12
|
+
const Checked = Schema.TaggedStruct("Checked", { cursor: Schema.NullOr(Schema.String) });
|
|
13
|
+
const fieldsOf = (name, data) => ({
|
|
14
|
+
eventId: Schema.String.annotate({ description: "The source's id for it, or one generated: a subscriber deduplicates by it." }),
|
|
15
|
+
name,
|
|
16
|
+
timestamp: Schema.DateTimeUtc,
|
|
17
|
+
data,
|
|
18
|
+
cursor: Schema.NullOr(Schema.String).annotate({ description: "Where to resume after it, null for an event type that cannot be resumed." })
|
|
19
|
+
});
|
|
20
|
+
/** An occurrence of `event`, as a subscriber decodes it. */
|
|
21
|
+
const OccurrenceOf = (event) => Schema.TaggedStruct("Occurrence", fieldsOf(Schema.Literal(event.name), event.payload));
|
|
22
|
+
/** Any occurrence, its data erased, as a feed makes it. */
|
|
23
|
+
const AnyOccurrence = Schema.TaggedStruct("Occurrence", fieldsOf(Schema.String, Schema.Unknown));
|
|
24
|
+
/** What a subscriber of `event` decodes: an occurrence, or a gap. */
|
|
25
|
+
const ReceivedOf = (event) => Schema.Union([OccurrenceOf(event), Gap]);
|
|
26
|
+
const untaggedOf = (event) => OccurrenceOf(event).mapFields((fields) => ({
|
|
27
|
+
...Struct.omit(fields, ["_tag"]),
|
|
28
|
+
timestamp: Schema.DateTimeUtcFromString,
|
|
29
|
+
data: Schema.toCodecJson(event.payload)
|
|
30
|
+
}));
|
|
31
|
+
/**
|
|
32
|
+
* How a subscriber of `event` decodes an occurrence sent as JSON without its tag, as MCP's
|
|
33
|
+
* notifications and a webhook's body carry one.
|
|
34
|
+
*/
|
|
35
|
+
const untaggedOccurrenceOf = (event) => {
|
|
36
|
+
const occurrence = OccurrenceOf(event);
|
|
37
|
+
const decode = Schema.decodeUnknownEffect(untaggedOf(event));
|
|
38
|
+
return (wire) => Effect.map(decode(wire), (fields) => occurrence.make(fields));
|
|
39
|
+
};
|
|
40
|
+
/** How a surface encodes an occurrence of `event` as JSON without its tag, its counterpart. */
|
|
41
|
+
const encodeUntaggedOf = (event) => {
|
|
42
|
+
const encode = Schema.encodeEffect(untaggedOf(event));
|
|
43
|
+
return (occurrence) => Effect.orDie(encode(occurrence));
|
|
44
|
+
};
|
|
45
|
+
export { AnyOccurrence, Checked, Gap, OccurrenceOf, ReceivedOf, encodeUntaggedOf, untaggedOccurrenceOf };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Checked } from "./occurrence.mjs";
|
|
2
|
+
import { Any, Occurrence, Received } from "../Event.mjs";
|
|
3
|
+
import { Duration, Effect, Stream } from "effect";
|
|
4
|
+
/** One poll's answer, as a surface sends it and a client reads it. */
|
|
5
|
+
export interface Page {
|
|
6
|
+
readonly events: ReadonlyArray<Occurrence<Any>>;
|
|
7
|
+
readonly cursor: string | null;
|
|
8
|
+
readonly truncated: boolean;
|
|
9
|
+
readonly hasMore: boolean;
|
|
10
|
+
readonly nextPoll: Duration.Duration;
|
|
11
|
+
}
|
|
12
|
+
/** What a feed's stream yields: what a subscriber receives, and where it has read up to. */
|
|
13
|
+
export type Fed = Received<Any> | Checked;
|