@gjermundgaraba/effect-actions 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -41
- package/dist/Action.d.mts +140 -46
- package/dist/Action.mjs +87 -42
- package/dist/ActionCli.d.mts +69 -33
- package/dist/ActionCli.mjs +51 -32
- package/dist/ActionHttp.d.mts +69 -35
- package/dist/ActionHttp.mjs +75 -23
- package/dist/ActionMcp.d.mts +56 -22
- package/dist/ActionMcp.mjs +94 -69
- package/dist/ActionRpc.d.mts +222 -55
- package/dist/ActionRpc.mjs +299 -46
- package/dist/ActionToolkit.d.mts +22 -5
- package/dist/ActionToolkit.mjs +36 -12
- package/dist/Authentication.d.mts +42 -10
- package/dist/Authentication.mjs +37 -30
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.mjs +1 -1
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -10
- package/dist/authentication/provider.mjs +1 -1
- package/dist/authentication/refusal.mjs +2 -2
- package/dist/cli/commands.d.mts +9 -5
- package/dist/cli/commands.mjs +84 -33
- package/dist/contract/ast.mjs +4 -1
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -6
- package/dist/contract/call.mjs +48 -6
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +13 -2
- package/dist/contract/caller.mjs +22 -3
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.mjs +25 -9
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -3
- package/dist/contract/errors.mjs +1 -1
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -47
- package/dist/contract/implementation.mjs +92 -58
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/names.mjs +14 -4
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +14 -5
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -12
- package/dist/http/client.mjs +51 -12
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +1 -1
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -97
- package/dist/mcp/client.mjs +59 -132
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +39 -13
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +13 -5
- package/dist/testing/memory.d.mts +1 -1
- package/dist/testing/memory.mjs +3 -3
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +52 -60
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -52
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +21 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +108 -47
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +10 -4
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import "./implementation.mjs";
|
|
2
|
+
import "../Action.mjs";
|
|
3
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
4
|
+
declare const Kept: Schema.Struct<{
|
|
5
|
+
readonly input: Schema.String;
|
|
6
|
+
readonly outcome: Schema.String;
|
|
7
|
+
readonly at: Schema.Finite;
|
|
8
|
+
}>;
|
|
9
|
+
type Kept = typeof Kept.Type;
|
|
10
|
+
declare const Idempotency_base: Context.ServiceClass<Idempotency, "effect-actions/Action/Idempotency", {
|
|
11
|
+
/** What was kept under `id`, or `undefined`; one kept longer than 24 hours may be either. */
|
|
12
|
+
readonly get: (id: string) => Effect.Effect<Kept | undefined>;
|
|
13
|
+
/** Keeps `kept` under `id`, replacing what was. */
|
|
14
|
+
readonly set: (id: string, kept: Kept) => Effect.Effect<void>;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Where the answers of calls given an idempotency key are kept, by the action's name and a
|
|
18
|
+
* SHA-256 digest in hex of the key and its caller's principal, `<action>/<digest>`, for 24 hours
|
|
19
|
+
* at least: what an implementation of an action naming `idempotencyKey` requires at startup.
|
|
20
|
+
* `layerIdempotency` keeps them in memory; a Durable Object's `make` and `handlers` provide one
|
|
21
|
+
* over its storage; another store, shared by several servers, implements `get` and `set`, keeping
|
|
22
|
+
* `Idempotency.Kept` at least `Idempotency.keptFor` milliseconds.
|
|
23
|
+
*
|
|
24
|
+
* **Gotchas**
|
|
25
|
+
*
|
|
26
|
+
* - Provide one per server, around the surfaces: an implementation built in another layer graph keeps its own.
|
|
27
|
+
* - A server waits for a repeat's first call itself, in memory: two servers sharing a store run a call each, the later answer kept.
|
|
28
|
+
*/
|
|
29
|
+
export declare class Idempotency extends Idempotency_base {
|
|
30
|
+
/**
|
|
31
|
+
* What is kept of a call answered under its key: the digest of its input, its outcome as JSON,
|
|
32
|
+
* and when it was kept, in milliseconds since the epoch.
|
|
33
|
+
*/
|
|
34
|
+
static readonly Kept: Schema.Struct<{
|
|
35
|
+
readonly input: Schema.String;
|
|
36
|
+
readonly outcome: Schema.String;
|
|
37
|
+
readonly at: Schema.Finite;
|
|
38
|
+
}>;
|
|
39
|
+
/** How long, in milliseconds, a kept answer is kept at least: 24 hours. */
|
|
40
|
+
static readonly keptFor: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `Idempotency` in the server's memory, each answer kept 24 hours after it was kept: lost when
|
|
44
|
+
* the server stops, and kept apart from every other server's.
|
|
45
|
+
*/
|
|
46
|
+
export declare const layerIdempotency: Layer.Layer<Idempotency>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { CallerKeyed, principalOf } from "./caller.mjs";
|
|
2
|
+
import { InvalidInput } from "./errors.mjs";
|
|
3
|
+
import { declaredStatus, isTransientStatus } from "./status.mjs";
|
|
4
|
+
import { projectedErrors } from "./declared.mjs";
|
|
5
|
+
import { answered } from "./implementation.mjs";
|
|
6
|
+
import { canonicalInput } from "./canonical.mjs";
|
|
7
|
+
import { sha256Hex } from "./digest.mjs";
|
|
8
|
+
import { Cause, Clock, Context, Deferred, Effect, Exit, Layer, Option, Predicate, Schema } from "effect";
|
|
9
|
+
// src/contract/idempotency.ts
|
|
10
|
+
const Kept = Schema.Struct({
|
|
11
|
+
input: Schema.String,
|
|
12
|
+
outcome: Schema.String,
|
|
13
|
+
at: Schema.Finite
|
|
14
|
+
});
|
|
15
|
+
const keptFor = 864e5;
|
|
16
|
+
/**
|
|
17
|
+
* Where the answers of calls given an idempotency key are kept, by the action's name and a
|
|
18
|
+
* SHA-256 digest in hex of the key and its caller's principal, `<action>/<digest>`, for 24 hours
|
|
19
|
+
* at least: what an implementation of an action naming `idempotencyKey` requires at startup.
|
|
20
|
+
* `layerIdempotency` keeps them in memory; a Durable Object's `make` and `handlers` provide one
|
|
21
|
+
* over its storage; another store, shared by several servers, implements `get` and `set`, keeping
|
|
22
|
+
* `Idempotency.Kept` at least `Idempotency.keptFor` milliseconds.
|
|
23
|
+
*
|
|
24
|
+
* **Gotchas**
|
|
25
|
+
*
|
|
26
|
+
* - Provide one per server, around the surfaces: an implementation built in another layer graph keeps its own.
|
|
27
|
+
* - A server waits for a repeat's first call itself, in memory: two servers sharing a store run a call each, the later answer kept.
|
|
28
|
+
*/
|
|
29
|
+
var Idempotency = class extends Context.Service()("effect-actions/Action/Idempotency") {
|
|
30
|
+
/**
|
|
31
|
+
* What is kept of a call answered under its key: the digest of its input, its outcome as JSON,
|
|
32
|
+
* and when it was kept, in milliseconds since the epoch.
|
|
33
|
+
*/
|
|
34
|
+
static Kept = Kept;
|
|
35
|
+
/** How long, in milliseconds, a kept answer is kept at least: 24 hours. */
|
|
36
|
+
static keptFor = keptFor;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* `Idempotency` in the server's memory, each answer kept 24 hours after it was kept: lost when
|
|
40
|
+
* the server stops, and kept apart from every other server's.
|
|
41
|
+
*/
|
|
42
|
+
const layerIdempotency = Layer.sync(Idempotency)(() => {
|
|
43
|
+
const held = /* @__PURE__ */ new Map();
|
|
44
|
+
return Idempotency.of({
|
|
45
|
+
get: (id) => Effect.sync(() => held.get(id)),
|
|
46
|
+
set: (id, kept) => Effect.sync(() => {
|
|
47
|
+
held.delete(id);
|
|
48
|
+
held.set(id, kept);
|
|
49
|
+
for (const [oldest, { at }] of held) {
|
|
50
|
+
if (at > kept.at - keptFor) break;
|
|
51
|
+
held.delete(oldest);
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* `handle`, answering a call given `action`'s idempotency key as the first call with it and the
|
|
58
|
+
* same input was answered: from `store`, or, while that call runs, once it has.
|
|
59
|
+
*/
|
|
60
|
+
const keyed = (action, store, handle) => {
|
|
61
|
+
const field = action.idempotencyKey;
|
|
62
|
+
if (field === void 0) return handle;
|
|
63
|
+
const Outcome = Schema.fromJsonString(Schema.toCodecJson(Schema.Exit(action.success, Schema.Union(projectedErrors(action)), Schema.Never)));
|
|
64
|
+
const encodeOutcome = Schema.encodeUnknownEffect(Outcome);
|
|
65
|
+
const decodeOutcome = Schema.decodeUnknownOption(Outcome);
|
|
66
|
+
const isTransientFailure = Schema.is(Schema.Union(projectedErrors(action).filter((error) => isTransientStatus(declaredStatus(error)))));
|
|
67
|
+
const running = /* @__PURE__ */ new Map();
|
|
68
|
+
const run = (id, key, digest, answer) => Effect.gen(function* () {
|
|
69
|
+
const first = running.get(id);
|
|
70
|
+
if (first !== void 0) {
|
|
71
|
+
yield* Deferred.await(first).pipe(Effect.withSpan("Idempotency.wait", { attributes: { "action.name": action.name } }));
|
|
72
|
+
return yield* run(id, key, digest, answer);
|
|
73
|
+
}
|
|
74
|
+
const claim = Deferred.makeUnsafe();
|
|
75
|
+
running.set(id, claim);
|
|
76
|
+
const release = Effect.andThen(Effect.sync(() => running.delete(id)), Deferred.succeed(claim, void 0));
|
|
77
|
+
return yield* Effect.gen(function* () {
|
|
78
|
+
const kept = yield* store.get(id);
|
|
79
|
+
if (kept !== void 0 && kept.input !== digest) return yield* new InvalidInput({ message: `The idempotency key ${key} of ${action.name} was given with another input (send a new key for another call)` });
|
|
80
|
+
const replayed = kept === void 0 ? Option.none() : decodeOutcome(kept.outcome);
|
|
81
|
+
if (Option.isSome(replayed)) return yield* replayed.value;
|
|
82
|
+
const exit = yield* Effect.exit(answer);
|
|
83
|
+
const json = Exit.isSuccess(exit) || exit.cause.reasons.every((reason) => Cause.isFailReason(reason) && !isTransientFailure(reason.error)) ? yield* Effect.option(encodeOutcome(exit)) : Option.none();
|
|
84
|
+
if (Option.isSome(json)) yield* store.set(id, {
|
|
85
|
+
input: digest,
|
|
86
|
+
outcome: json.value,
|
|
87
|
+
at: yield* Clock.currentTimeMillis
|
|
88
|
+
});
|
|
89
|
+
return yield* exit;
|
|
90
|
+
}).pipe(Effect.ensuring(release));
|
|
91
|
+
});
|
|
92
|
+
return (input, reporting) => {
|
|
93
|
+
const key = Predicate.hasProperty(input, field) ? input[field] : void 0;
|
|
94
|
+
if (!Predicate.isString(key)) return handle(input, reporting);
|
|
95
|
+
const answer = Effect.suspend(() => answered(handle(input, reporting)));
|
|
96
|
+
return Effect.flatMap(Effect.all([Effect.service(CallerKeyed).pipe(Effect.flatMap((named) => principalOf(action, named)), Effect.flatMap((principal) => sha256Hex(JSON.stringify([principal ?? null, key])))), Effect.flatMap(canonicalInput(action.input, input), sha256Hex)]), ([id, digest]) => run(`${action.name}/${id}`, key, digest, answer));
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
export { Idempotency, keyed, layerIdempotency };
|
|
@@ -1,27 +1,41 @@
|
|
|
1
|
-
import { Anyone } from "./caller.mjs";
|
|
1
|
+
import { Anyone, Keyed } from "./caller.mjs";
|
|
2
2
|
import { Refusal } from "./errors.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { AnyEventImplementation, EventOf } from "./feed.mjs";
|
|
4
|
+
import { Any } from "../Event.mjs";
|
|
5
|
+
import { Reporting } from "./progress.mjs";
|
|
6
|
+
import { Any as Any$1 } from "../Action.mjs";
|
|
7
|
+
import { Context, Effect, Layer, Scope, Stream } from "effect";
|
|
5
8
|
/** Decoded values at the dispatch boundary every surface shares. */
|
|
6
|
-
export type ErasedValue = Any["input"]["Type"];
|
|
9
|
+
export type ErasedValue = Any$1["input"]["Type"];
|
|
7
10
|
/**
|
|
8
|
-
* A handler, erased
|
|
9
|
-
*
|
|
10
|
-
* handlers are not widened at
|
|
11
|
+
* A handler as written, erased: its answer, or the stream of an action answering one. Method
|
|
12
|
+
* extraction makes the argument bivariant: a surface selects the action before it invokes a
|
|
13
|
+
* handler, so its decoded input is the matching handler input, and handlers are not widened at
|
|
14
|
+
* their public API.
|
|
11
15
|
*/
|
|
12
|
-
export type
|
|
13
|
-
handle(input: ErasedValue): Effect.Effect<ErasedValue, ErasedValue, R>;
|
|
16
|
+
export type WrittenHandler<R> = {
|
|
17
|
+
handle(input: ErasedValue, reporting?: Reporting): Effect.Effect<ErasedValue, ErasedValue, R> | Stream.Stream<ErasedValue, ErasedValue, R>;
|
|
14
18
|
}["handle"];
|
|
19
|
+
/**
|
|
20
|
+
* A handler behind its action's authentication, authorization and declared errors, as a surface
|
|
21
|
+
* calls it. For an action answering a stream it succeeds at once with the call's stream, which
|
|
22
|
+
* `elementsOf` reads, the refusals included. `reporting` is where its progress goes, ignored for
|
|
23
|
+
* an action declaring no `progress`, so a surface may hand one to every call.
|
|
24
|
+
*/
|
|
25
|
+
export type ErasedHandler<R> = (input: ErasedValue, reporting?: Reporting) => Effect.Effect<ErasedValue, ErasedValue, R>;
|
|
15
26
|
/**
|
|
16
27
|
* Authorization for protected contracts: it refuses, and fails with nothing else. It runs only for
|
|
17
28
|
* protected actions, after authentication and before the handler.
|
|
18
29
|
*/
|
|
19
|
-
export type Authorize<A extends Any, R = never> = (action: A) => Effect.Effect<void, Refusal, R>;
|
|
20
|
-
/** An
|
|
21
|
-
export type ErasedAuthorize = (
|
|
22
|
-
/** Each action of an implementation with its handler, as
|
|
23
|
-
export type Bound = ReadonlyArray<readonly [Any, ErasedHandler<unknown>]>;
|
|
24
|
-
|
|
30
|
+
export type Authorize<A extends Any$1, R = never> = (action: A) => Effect.Effect<void, Refusal, R>;
|
|
31
|
+
/** An authorization rule over contracts `C`, erased: it refuses, or lets the call run. */
|
|
32
|
+
export type ErasedAuthorize<C = Any$1> = (contract: C) => Effect.Effect<void, unknown, unknown>;
|
|
33
|
+
/** Each action of an implementation with its handler, as `dispatch` makes it. */
|
|
34
|
+
export type Bound = ReadonlyArray<readonly [Any$1, ErasedHandler<unknown>]>;
|
|
35
|
+
/** Each action of an implementation with its handler as written, as its builder made them. */
|
|
36
|
+
export type Written = ReadonlyArray<readonly [Any$1, WrittenHandler<unknown>]>;
|
|
37
|
+
/** A build shared by every layer an implementation reaches, through the one key it is provided under. */
|
|
38
|
+
export interface Memoized<S, E, R> {
|
|
25
39
|
readonly key: Context.Key<S, S>;
|
|
26
40
|
readonly layer: Layer.Layer<S, E, R>;
|
|
27
41
|
}
|
|
@@ -34,7 +48,7 @@ interface Memoized<S, E, R> {
|
|
|
34
48
|
* its handlers' builder, `RA` what its authorizer reads per request, and `EAX` and `RAX` the
|
|
35
49
|
* failures and services of the authorizer's builder.
|
|
36
50
|
*/
|
|
37
|
-
export declare class Implementation<A extends Any, RequestServices extends {
|
|
51
|
+
export declare class Implementation<A extends Any$1, RequestServices extends {
|
|
38
52
|
readonly [name: string]: unknown;
|
|
39
53
|
}, EX, RX, RA = never, EAX = never, RAX = never> {
|
|
40
54
|
#private;
|
|
@@ -51,14 +65,14 @@ export declare class Implementation<A extends Any, RequestServices extends {
|
|
|
51
65
|
/** Type-only: what building its authorizer needs. */
|
|
52
66
|
readonly "~authorizeBuildServices": RAX;
|
|
53
67
|
/** The contracts this implementation answers. */
|
|
54
|
-
readonly actions:
|
|
55
|
-
constructor(actions:
|
|
68
|
+
readonly actions: readonly A[];
|
|
69
|
+
constructor(actions: readonly A[],
|
|
56
70
|
/** Builds each action paired with its handler: its own builder, or the one it shares. */
|
|
57
|
-
handlers: Memoized<
|
|
71
|
+
handlers: Memoized<Written, EX, RX>,
|
|
58
72
|
/** Builds its authorizer. */
|
|
59
73
|
authorizer: Memoized<ErasedAuthorize, EAX, RAX>);
|
|
60
74
|
/** Select contracts while retaining their handlers, authorization and shared builder. */
|
|
61
|
-
static share(actions:
|
|
75
|
+
static share(actions: readonly Any$1[], app: AnyImplementation): AnyImplementation;
|
|
62
76
|
/**
|
|
63
77
|
* The key of what runs a call of `app`'s actions: its handlers' memoized layer's, which each
|
|
64
78
|
* `implement` makes, so it is unique to the implementation. Every selection retains it.
|
|
@@ -74,7 +88,7 @@ export declare class Implementation<A extends Any, RequestServices extends {
|
|
|
74
88
|
* The actions of `app` with their handlers, behind its authorizer, from the context
|
|
75
89
|
* `layerOf(app)` provides.
|
|
76
90
|
*/
|
|
77
|
-
static boundOf(app: AnyImplementation): Effect.Effect<Bound, never, unknown>;
|
|
91
|
+
static boundOf(app: AnyImplementation, keyed: Keyed | undefined): Effect.Effect<Bound, never, unknown>;
|
|
78
92
|
/** `app`, if this copy of the module made it; another installed copy's cannot be read. */
|
|
79
93
|
private static own;
|
|
80
94
|
}
|
|
@@ -85,43 +99,55 @@ export declare class Implementation<A extends Any, RequestServices extends {
|
|
|
85
99
|
* admits every implementation, while a value of this type owes `unknown`, which no surface can be
|
|
86
100
|
* given.
|
|
87
101
|
*/
|
|
88
|
-
export type AnyImplementation<A extends Any = Any> = Implementation<A, {
|
|
102
|
+
export type AnyImplementation<A extends Any$1 = Any$1> = Implementation<A, {
|
|
89
103
|
readonly [name: string]: unknown;
|
|
90
104
|
}, unknown, unknown, unknown, unknown, unknown>;
|
|
91
|
-
/** What a surface serves: one implementation, or a list of them. */
|
|
92
|
-
export type Served = AnyImplementation | ReadonlyArray<AnyImplementation>;
|
|
105
|
+
/** What a surface serves: one implementation, of actions or of events, or a list of them. */
|
|
106
|
+
export type Served = AnyImplementation | AnyEventImplementation | ReadonlyArray<AnyImplementation | AnyEventImplementation>;
|
|
107
|
+
/** What a surface serving no events takes: one action implementation, or a list of them. */
|
|
108
|
+
export type ServedActions = AnyImplementation | readonly AnyImplementation[];
|
|
93
109
|
/** The implementations `S` stands for. */
|
|
94
110
|
export type Member<S> = S extends ReadonlyArray<infer App> ? App : S;
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Options that may list actions and event types of `C`, the shape every selecting surface's
|
|
113
|
+
* options share.
|
|
114
|
+
*/
|
|
115
|
+
export interface Selection<C extends Any$1 | Any> {
|
|
116
|
+
readonly actions?: readonly C[] | undefined;
|
|
98
117
|
}
|
|
99
118
|
/**
|
|
100
119
|
* The actions options `O` select, of `All`: those its `actions` lists when it always lists
|
|
101
|
-
* them, and every one otherwise. Options whose `actions`
|
|
102
|
-
* a union with options lacking it, select every action at run
|
|
120
|
+
* them, none where it lists event types alone, and every one otherwise. Options whose `actions`
|
|
121
|
+
* may be absent, being optional or a union with options lacking it, select every action at run
|
|
122
|
+
* time, so they narrow nothing.
|
|
103
123
|
*/
|
|
104
|
-
export type Selected<O, All extends Any> = O extends {
|
|
105
|
-
readonly actions: ReadonlyArray<infer
|
|
106
|
-
} ?
|
|
124
|
+
export type Selected<O, All extends Any$1> = O extends {
|
|
125
|
+
readonly actions: ReadonlyArray<infer C>;
|
|
126
|
+
} ? Extract<C, Any$1> : All;
|
|
127
|
+
/** The event types options `O` select, of `All`, as `Selected` selects actions. */
|
|
128
|
+
export type SelectedEvents<O, All extends Any> = O extends {
|
|
129
|
+
readonly actions: ReadonlyArray<infer C>;
|
|
130
|
+
} ? Extract<C, Any> : All;
|
|
107
131
|
/**
|
|
108
132
|
* The middleware options `O` install, each a member of `L`: its `middleware` when always given,
|
|
109
133
|
* an array of unknown length when it may be absent, which installs none or all of them, and
|
|
110
134
|
* none otherwise.
|
|
111
135
|
*/
|
|
112
|
-
export type MiddlewareOf<O, L extends
|
|
136
|
+
export type MiddlewareOf<O, L extends readonly unknown[]> = O extends {
|
|
113
137
|
readonly middleware: infer M extends L;
|
|
114
138
|
} ? M : O extends {
|
|
115
139
|
readonly middleware?: infer M extends L | undefined;
|
|
116
140
|
} ? "middleware" extends keyof O ? ReadonlyArray<NonNullable<M>[number]> : [] : [];
|
|
117
141
|
/** The actions options `O` select of the implementations `Apps` stand for. */
|
|
118
142
|
export type SelectedOf<O, Apps extends Served> = Selected<O, ActionOf<Member<Apps>>>;
|
|
143
|
+
/** The event types options `O` select of the implementations `Apps` stand for. */
|
|
144
|
+
export type SelectedEventsOf<O, Apps extends Served> = SelectedEvents<O, EventOf<Member<Apps>>>;
|
|
119
145
|
/**
|
|
120
|
-
* The actions a client of options `O` offers
|
|
146
|
+
* The actions or event types a client of options `O` offers, of `All`: those `actions` may
|
|
121
147
|
* list, wherever some member of `O` may list them, as each is present whether the list is
|
|
122
148
|
* given or not; every one where no member lists any.
|
|
123
149
|
*/
|
|
124
|
-
export type Offered<O, All extends Any> = [Listing<O>] extends [never] ? All : Extract<All, Listing<O> extends ReadonlyArray<infer
|
|
150
|
+
export type Offered<O, All extends Any$1 | Any> = [Listing<O>] extends [never] ? All : Extract<All, Listing<O> extends ReadonlyArray<infer C> ? C : never>;
|
|
125
151
|
type Listing<O> = O extends unknown ? NonNullable<O["actions" & keyof O]> : never;
|
|
126
152
|
/**
|
|
127
153
|
* No key of any member of `O` beyond those of `Keys`, so a misspelled option is refused
|
|
@@ -139,7 +165,7 @@ export type OptionalUnless<O, Given> = {} extends O ? [options?: Given] : [optio
|
|
|
139
165
|
export type ActionOf<App> = App extends {
|
|
140
166
|
readonly actions: ReadonlyArray<infer A>;
|
|
141
167
|
} ? A : never;
|
|
142
|
-
type Matches<A extends Any, Listed extends Any> = Listed extends Any ? [A] extends [Listed] ? true : [Listed] extends [A] ? true : never : never;
|
|
168
|
+
type Matches<A extends Any$1, Listed extends Any$1> = Listed extends Any$1 ? [A] extends [Listed] ? true : [Listed] extends [A] ? true : never : never;
|
|
143
169
|
/**
|
|
144
170
|
* The actions of `App` a surface given the actions `Listed` may serve, as `select` selects
|
|
145
171
|
* them by identity: an action whose type either is assignable to, or is assigned by, one
|
|
@@ -147,34 +173,34 @@ type Matches<A extends Any, Listed extends Any> = Listed extends Any ? [A] exten
|
|
|
147
173
|
* actions, an erased one included, still counts, and another contract of a listed name, which
|
|
148
174
|
* the surface leaves out beside an action it serves, does not.
|
|
149
175
|
*/
|
|
150
|
-
export type Serving<App, Listed extends Any> = ActionOf<App> extends (infer A extends Any) ? A extends unknown ? [A] extends [Listed] ? A : string extends A["name"] ? A : [Extract<A["name"], Listed["name"]>] extends [never] ? never : [Matches<A, Listed>] extends [never] ? never : A : never : never;
|
|
176
|
+
export type Serving<App, Listed extends Any$1> = ActionOf<App> extends (infer A extends Any$1) ? A extends unknown ? [A] extends [Listed] ? A : string extends A["name"] ? A : [Extract<A["name"], Listed["name"]>] extends [never] ? never : [Matches<A, Listed>] extends [never] ? never : A : never : never;
|
|
151
177
|
/**
|
|
152
178
|
* What each implementation among `App` owes per request where a surface serving the actions
|
|
153
179
|
* `Listed` serves it: its authorizer's services, and the handlers' of the actions it serves.
|
|
154
180
|
*/
|
|
155
|
-
export type ServedRequest<App, Listed extends Any> = App extends unknown ? RequestOf<App, Serving<App, Listed>> : never;
|
|
181
|
+
export type ServedRequest<App, Listed extends Any$1> = App extends unknown ? RequestOf<App, Serving<App, Listed>> : never;
|
|
156
182
|
/** The implementations among `App` holding an action of `Listed`: those a surface builds. */
|
|
157
|
-
export type Holding<App, Listed extends Any> = App extends unknown ? [Serving<App, Listed>] extends [never] ? never : App : never;
|
|
158
|
-
/** The protected members of a contract union. */
|
|
159
|
-
export type Protected<A extends Any
|
|
183
|
+
export type Holding<App, Listed extends Any$1> = App extends unknown ? [Serving<App, Listed>] extends [never] ? never : App : never;
|
|
184
|
+
/** The protected members of a contract union, of actions or of events. */
|
|
185
|
+
export type Protected<A extends Pick<Any$1, "caller">> = A extends unknown ? A["caller"] extends typeof Anyone ? never : A : never;
|
|
160
186
|
type ServiceOf<K> = K extends Context.Key<infer I, unknown> ? I : never;
|
|
161
|
-
type AuthenticationOf<A extends Any> = A["caller"] extends typeof Anyone ? never : ServiceOf<A["caller"]>;
|
|
187
|
+
type AuthenticationOf<A extends Any$1> = A["caller"] extends typeof Anyone ? never : ServiceOf<A["caller"]>;
|
|
162
188
|
/**
|
|
163
189
|
* Per-request requirements of `App`'s authorization and handler for each `A` it
|
|
164
190
|
* implements: those of every name `A`'s may be, so a union or an erased name owes each it may
|
|
165
191
|
* stand for.
|
|
166
192
|
*/
|
|
167
|
-
export type RequestOf<App, A extends Any> = App extends {
|
|
193
|
+
export type RequestOf<App, A extends Any$1> = App extends {
|
|
168
194
|
readonly "~request": infer R;
|
|
169
195
|
readonly "~authorizeRequest": infer RA;
|
|
170
|
-
} ? A extends Any ? [A["name"] & keyof R] extends [never] ? never : R[A["name"] & keyof R] | (A["caller"] extends typeof Anyone ? never : RA) | AuthenticationOf<A> : never : never;
|
|
171
|
-
type OwnActions<App> = Extract<ActionOf<App>, Any>;
|
|
196
|
+
} ? A extends Any$1 ? [A["name"] & keyof R] extends [never] ? never : R[A["name"] & keyof R] | (A["caller"] extends typeof Anyone ? never : RA) | AuthenticationOf<A> : never : never;
|
|
197
|
+
type OwnActions<App> = Extract<ActionOf<App>, Any$1>;
|
|
172
198
|
/**
|
|
173
199
|
* What a surface serving the actions `Listed` of the implementations `App` fails with at startup:
|
|
174
200
|
* their builders' and built authorizers' failures, an authorizer's only if a selected protected
|
|
175
201
|
* action needs it.
|
|
176
202
|
*/
|
|
177
|
-
export type BuildError<App, Listed extends Any = OwnActions<App>> = App extends {
|
|
203
|
+
export type BuildError<App, Listed extends Any$1 = OwnActions<App>> = App extends {
|
|
178
204
|
readonly "~buildError": infer EX;
|
|
179
205
|
readonly "~authorizeBuildError": infer EAX;
|
|
180
206
|
} ? EX | ([Protected<Serving<App, Listed>>] extends [never] ? never : EAX) : never;
|
|
@@ -182,7 +208,7 @@ export type BuildError<App, Listed extends Any = OwnActions<App>> = App extends
|
|
|
182
208
|
* What a surface serving the actions `Listed` of the implementations `App` reads at startup: their
|
|
183
209
|
* builders' and built authorizers' services.
|
|
184
210
|
*/
|
|
185
|
-
export type BuildServices<App, Listed extends Any = OwnActions<App>> = App extends {
|
|
211
|
+
export type BuildServices<App, Listed extends Any$1 = OwnActions<App>> = App extends {
|
|
186
212
|
readonly "~buildServices": infer RX;
|
|
187
213
|
readonly "~authorizeBuildServices": infer RAX;
|
|
188
214
|
} ? RX | ([Protected<Serving<App, Listed>>] extends [never] ? never : RAX) : never;
|
|
@@ -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 {
|
|
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";
|
|
6
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
|
|
@@ -50,28 +105,32 @@ var Implementation = class Implementation {
|
|
|
50
105
|
*/
|
|
51
106
|
static layerOf(app) {
|
|
52
107
|
const source = Implementation.own(app);
|
|
53
|
-
return app.actions
|
|
108
|
+
return builderLayer(app.actions, source.#handlers, source.#authorizer);
|
|
54
109
|
}
|
|
55
110
|
/**
|
|
56
111
|
* The actions of `app` with their handlers, behind its authorizer, from the context
|
|
57
112
|
* `layerOf(app)` provides.
|
|
58
113
|
*/
|
|
59
|
-
static boundOf(app) {
|
|
114
|
+
static boundOf(app, keyed) {
|
|
60
115
|
const { actions } = app;
|
|
61
116
|
const source = Implementation.own(app);
|
|
62
117
|
return Effect.gen(function* () {
|
|
63
118
|
const bound = yield* source.#handlers.key;
|
|
64
119
|
const before = actions.some((action) => action.caller !== Anyone) ? yield* source.#authorizer.key : () => Effect.void;
|
|
65
|
-
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)]] : []));
|
|
66
121
|
});
|
|
67
122
|
}
|
|
68
123
|
/** `app`, if this copy of the module made it; another installed copy's cannot be read. */
|
|
69
124
|
static own(app) {
|
|
70
|
-
if (!(#handlers in app)) throw
|
|
125
|
+
if (!(#handlers in app)) throw notMadeHere("Action");
|
|
71
126
|
return app;
|
|
72
127
|
}
|
|
73
128
|
};
|
|
74
|
-
|
|
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"));
|
|
75
134
|
const notHeld = {
|
|
76
135
|
byImplementation: {
|
|
77
136
|
why: "no implementation holds it",
|
|
@@ -94,15 +153,22 @@ const assertHeld = ({ why, fix, holder }, listed, held) => {
|
|
|
94
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}` : ""})`);
|
|
95
154
|
}
|
|
96
155
|
};
|
|
97
|
-
|
|
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) => {
|
|
98
162
|
if (listed === void 0) return apps;
|
|
99
|
-
assertHeld(notHeld.byImplementation, listed, apps.flatMap(
|
|
163
|
+
assertHeld(notHeld.byImplementation, listed, apps.flatMap(holds));
|
|
100
164
|
return apps.flatMap((app) => {
|
|
101
|
-
const
|
|
165
|
+
const held = holds(app);
|
|
166
|
+
const own = held.filter((contract) => listed.includes(contract));
|
|
102
167
|
if (own.length === 0) return [];
|
|
103
|
-
return own.length ===
|
|
168
|
+
return own.length === held.length ? [app] : [share(own, app)];
|
|
104
169
|
});
|
|
105
170
|
};
|
|
171
|
+
const select = (apps, listed) => selectHeld(apps, listed, (app) => app.actions, (own, app) => Implementation.share(own, app));
|
|
106
172
|
const servedBy = (what, bound, apps, listed) => {
|
|
107
173
|
if (listed !== void 0) assertHeld(notHeld.byBinding, listed, bound);
|
|
108
174
|
const held = apps.flatMap((app) => app.actions);
|
|
@@ -115,58 +181,26 @@ const servedBy = (what, bound, apps, listed) => {
|
|
|
115
181
|
assertOnce("served action", actions, heldOnce);
|
|
116
182
|
return served;
|
|
117
183
|
};
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
*/
|
|
122
|
-
const servingOf = (what, binding, served, options) => {
|
|
123
|
-
const apps = servedBy(what, binding.actions, toList(served), options.actions);
|
|
124
|
-
const actions = apps.flatMap((app) => app.actions);
|
|
125
|
-
assertAuthentication(actions, binding.authentication);
|
|
126
|
-
assertDistinct("middleware", options.middleware ?? [], (key) => key.key, { fix: "list each middleware once" });
|
|
127
|
-
return {
|
|
128
|
-
apps,
|
|
129
|
-
actions,
|
|
130
|
-
authentication: actions.some((action) => action.caller !== Anyone) ? binding.authentication : void 0
|
|
131
|
-
};
|
|
132
|
-
};
|
|
133
|
-
const provideHandlers = (apps) => (layer) => {
|
|
134
|
-
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;
|
|
135
187
|
return first === void 0 ? layer : Layer.provide(layer, Layer.mergeAll(first, ...rest));
|
|
136
188
|
};
|
|
137
|
-
const
|
|
138
|
-
const acquire = (apps) => Effect.map(Effect.forEach(apps, (app) => Implementation.boundOf(app)), (bound) => bound.flat());
|
|
139
|
-
|
|
140
|
-
|
|
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;
|
|
141
197
|
return Effect.gen(function* () {
|
|
142
|
-
if (first === void 0) return
|
|
198
|
+
if (first === void 0) return yield* acquisition;
|
|
143
199
|
const current = yield* Effect.serviceOption(Layer.CurrentMemoMap);
|
|
144
200
|
const memoMap = Option.getOrElse(current, Layer.makeMemoMapUnsafe);
|
|
145
201
|
const scope = yield* Effect.scope;
|
|
146
202
|
const context = yield* Layer.buildWithMemoMap(Layer.mergeAll(first, ...rest), memoMap, scope);
|
|
147
|
-
return yield* Effect.provideContext(
|
|
148
|
-
});
|
|
149
|
-
};
|
|
150
|
-
const dispatch = (action, handle, before) => {
|
|
151
|
-
const actionAttributes = {
|
|
152
|
-
"action.name": action.name,
|
|
153
|
-
"action.read_only": action.readOnly
|
|
154
|
-
};
|
|
155
|
-
const authentication = action.caller === Anyone ? Effect.void : Effect.flatMap(Effect.serviceOption(action.caller), (actor) => Option.isSome(actor) ? Effect.void : Effect.fail(new Unauthenticated()));
|
|
156
|
-
const authorization = action.caller === Anyone ? Effect.void : Effect.suspend(() => before(action));
|
|
157
|
-
const validate = Schema.decodeUnknownEffect(Schema.toType(Schema.Union(projectedErrors(action))));
|
|
158
|
-
const asDefects = (reason, rejected) => {
|
|
159
|
-
const trace = Context.makeUnsafe(reason.annotations);
|
|
160
|
-
return [Cause.makeDieReason(reason.error).annotate(trace), Cause.makeDieReason(Cause.squash(rejected)).annotate(trace)];
|
|
161
|
-
};
|
|
162
|
-
const declaredOrDefect = Effect.fnUntraced(function* (cause) {
|
|
163
|
-
const reasons = [];
|
|
164
|
-
for (const reason of cause.reasons) if (Cause.isFailReason(reason)) {
|
|
165
|
-
const validated = yield* Effect.exit(validate(reason.error));
|
|
166
|
-
reasons.push(...Exit.isSuccess(validated) ? [reason] : asDefects(reason, validated.cause));
|
|
167
|
-
} else reasons.push(reason);
|
|
168
|
-
return yield* Effect.failCause(Cause.fromReasons(reasons));
|
|
203
|
+
return yield* Effect.provideContext(acquisition, context);
|
|
169
204
|
});
|
|
170
|
-
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 }));
|
|
171
205
|
};
|
|
172
|
-
export { Implementation, acquire, assertHeld,
|
|
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 };
|