@gjermundgaraba/effect-actions 0.10.0 → 0.12.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 +103 -43
- package/dist/Action.d.mts +31 -118
- package/dist/Action.mjs +15 -53
- package/dist/ActionCli.d.mts +60 -28
- package/dist/ActionCli.mjs +54 -48
- package/dist/ActionHttp.d.mts +18 -67
- package/dist/ActionHttp.mjs +20 -70
- package/dist/ActionMcp.d.mts +25 -31
- package/dist/ActionMcp.mjs +140 -74
- package/dist/ActionRpc.d.mts +147 -0
- package/dist/ActionRpc.mjs +113 -0
- package/dist/ActionToolkit.d.mts +5 -8
- package/dist/ActionToolkit.mjs +3 -3
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +32 -25
- package/dist/Testing.mjs +45 -30
- package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
- package/dist/authentication/provider.mjs +12 -0
- package/dist/authentication/refusal.mjs +36 -0
- package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
- package/dist/cli/commands.mjs +302 -0
- package/dist/{internal → contract}/call.d.mts +2 -2
- package/dist/contract/call.mjs +10 -0
- package/dist/{internal → contract}/errors.d.mts +13 -10
- package/dist/{internal → contract}/errors.mjs +9 -14
- package/dist/{internal → contract}/implementation.d.mts +37 -21
- package/dist/{internal → contract}/implementation.mjs +18 -49
- package/dist/contract/media.d.mts +13 -0
- package/dist/contract/media.mjs +18 -0
- package/dist/contract/rules.d.mts +25 -0
- package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
- package/dist/{internal → http}/client.d.mts +7 -9
- package/dist/http/client.mjs +17 -0
- package/dist/http/declared.mjs +20 -0
- package/dist/http/schema-errors.mjs +9 -0
- package/dist/mcp/protocol.d.mts +17 -0
- package/dist/mcp/protocol.mjs +149 -0
- package/dist/stdio/console.mjs +64 -0
- package/dist/{internal → testing}/memory.d.mts +1 -1
- package/dist/{internal → testing}/memory.mjs +5 -17
- package/dist/toolkit/tools.mjs +29 -0
- package/docs/Action-failures.md +47 -0
- package/docs/Action-in-process.md +93 -0
- package/docs/Action.md +206 -424
- package/docs/ActionCli-failures.md +102 -0
- package/docs/ActionCli.md +206 -292
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +202 -281
- package/docs/ActionMcp-failures.md +31 -0
- package/docs/ActionMcp.md +229 -193
- package/docs/ActionRpc-failures.md +21 -0
- package/docs/ActionRpc.md +178 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +100 -117
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +142 -392
- package/docs/CONTEXT.md +39 -23
- package/docs/README.md +71 -35
- package/docs/Testing-failures.md +20 -0
- package/docs/Testing.md +186 -151
- package/docs/guarantees.md +35 -95
- package/docs/setup-failures.md +34 -0
- package/docs/setup.md +88 -87
- package/examples/README.md +68 -0
- package/examples/assistant/README.md +92 -0
- package/examples/assistant/src/agent.ts +92 -0
- package/examples/assistant/src/cli.ts +34 -0
- package/examples/assistant/src/commands.ts +25 -0
- package/examples/assistant/src/identity/Identity.ts +33 -0
- package/examples/assistant/src/identity/authorize.ts +24 -0
- package/examples/assistant/src/identity/handlers.ts +10 -0
- package/examples/assistant/src/identity/members.ts +9 -0
- package/examples/assistant/src/identity/permissions.ts +23 -0
- package/examples/assistant/src/scripted.ts +23 -0
- package/examples/assistant/src/tasks/Tasks.ts +63 -0
- package/examples/assistant/src/tasks/handlers.ts +25 -0
- package/examples/assistant/src/tasks/store.ts +96 -0
- package/examples/assistant/src/toolkit.ts +30 -0
- package/examples/assistant/test/commands.test.ts +57 -0
- package/examples/assistant/test/identity/identity.test.ts +43 -0
- package/examples/assistant/test/tasks/store.test.ts +48 -0
- package/examples/assistant/test/tasks/tasks.test.ts +75 -0
- package/examples/assistant/test/toolkit.test.ts +121 -0
- package/examples/kitchen-sink/README.md +317 -0
- package/examples/kitchen-sink/src/Actions.ts +19 -0
- package/examples/kitchen-sink/src/Api.ts +24 -0
- package/examples/kitchen-sink/src/Rpc.ts +12 -0
- package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
- package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
- package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
- package/examples/kitchen-sink/src/admin/grants.ts +22 -0
- package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
- package/examples/kitchen-sink/src/admin/keys.ts +8 -0
- package/examples/kitchen-sink/src/agent.ts +47 -0
- package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
- package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
- package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
- package/examples/kitchen-sink/src/assistant/handlers.ts +146 -0
- package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
- package/examples/kitchen-sink/src/attachments/download.ts +49 -0
- package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
- package/examples/kitchen-sink/src/attachments/store.ts +58 -0
- package/examples/kitchen-sink/src/browser.ts +33 -0
- package/examples/kitchen-sink/src/cli.ts +85 -0
- package/examples/kitchen-sink/src/config.ts +18 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
- package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
- package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
- package/examples/kitchen-sink/src/identity/directory.ts +52 -0
- package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
- package/examples/kitchen-sink/src/identity/members.ts +13 -0
- package/examples/kitchen-sink/src/identity/quota.ts +62 -0
- package/examples/kitchen-sink/src/issues/Issues.ts +109 -0
- package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
- package/examples/kitchen-sink/src/issues/guide.ts +37 -0
- package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
- package/examples/kitchen-sink/src/issues/store.ts +96 -0
- package/examples/kitchen-sink/src/issues/write-limit.ts +41 -0
- package/examples/kitchen-sink/src/memory.ts +30 -0
- package/examples/kitchen-sink/src/ops.ts +42 -0
- package/examples/kitchen-sink/src/request-policy.ts +63 -0
- package/examples/kitchen-sink/src/routes.ts +120 -0
- package/examples/kitchen-sink/src/sdk.ts +46 -0
- package/examples/kitchen-sink/src/seed.ts +65 -0
- package/examples/kitchen-sink/src/server.ts +62 -0
- package/examples/kitchen-sink/src/stdio.ts +34 -0
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
- package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
- package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
- package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
- package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
- package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
- package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
- package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
- package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
- package/examples/kitchen-sink/test/routes.test.ts +123 -0
- package/examples/kitchen-sink/test/support/scripted.ts +40 -0
- package/examples/live-rpc/README.md +107 -0
- package/examples/live-rpc/src/Rpc.ts +8 -0
- package/examples/live-rpc/src/browser.ts +98 -0
- package/examples/live-rpc/src/chat/Chat.ts +75 -0
- package/examples/live-rpc/src/chat/authorize.ts +16 -0
- package/examples/live-rpc/src/chat/handlers.ts +50 -0
- package/examples/live-rpc/src/chat/room.ts +23 -0
- package/examples/live-rpc/src/chat/store.ts +54 -0
- package/examples/live-rpc/src/client.ts +37 -0
- package/examples/live-rpc/src/identity/Identity.ts +19 -0
- package/examples/live-rpc/src/identity/members.ts +8 -0
- package/examples/live-rpc/src/identity/verifier.ts +23 -0
- package/examples/live-rpc/src/routes.ts +22 -0
- package/examples/live-rpc/src/server.ts +43 -0
- package/examples/live-rpc/test/browser.test.ts +105 -0
- package/examples/live-rpc/test/chat/chat.test.ts +69 -0
- package/examples/live-rpc/test/routes.test.ts +99 -0
- package/examples/live-rpc/test/server.test.ts +75 -0
- package/examples/mcp-tools/README.md +136 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +119 -0
- package/examples/mcp-tools/src/printers/fake.ts +115 -0
- package/examples/mcp-tools/src/printers/farm.ts +136 -0
- package/examples/mcp-tools/src/printers/handlers.ts +29 -0
- package/examples/mcp-tools/src/routes.ts +52 -0
- package/examples/mcp-tools/src/server.ts +30 -0
- package/examples/mcp-tools/src/stdio.ts +26 -0
- package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
- package/examples/mcp-tools/test/routes.test.ts +202 -0
- package/examples/notes-service/README.md +108 -0
- package/examples/notes-service/src/Api.ts +13 -0
- package/examples/notes-service/src/browser.ts +41 -0
- package/examples/notes-service/src/cli.ts +22 -0
- package/examples/notes-service/src/commands.ts +89 -0
- package/examples/notes-service/src/identity/Identity.ts +31 -0
- package/examples/notes-service/src/identity/authorize.ts +18 -0
- package/examples/notes-service/src/identity/handlers.ts +10 -0
- package/examples/notes-service/src/identity/users.ts +9 -0
- package/examples/notes-service/src/identity/verifier.ts +36 -0
- package/examples/notes-service/src/notes/Notes.ts +91 -0
- package/examples/notes-service/src/notes/handlers.ts +33 -0
- package/examples/notes-service/src/notes/store.ts +73 -0
- package/examples/notes-service/src/routes.ts +45 -0
- package/examples/notes-service/src/server.ts +26 -0
- package/examples/notes-service/src/status/Status.ts +10 -0
- package/examples/notes-service/src/status/handlers.ts +14 -0
- package/examples/notes-service/src/stdio.ts +25 -0
- package/examples/notes-service/test/commands.test.ts +134 -0
- package/examples/notes-service/test/notes/notes.test.ts +103 -0
- package/examples/notes-service/test/routes.test.ts +84 -0
- package/examples/quickstart/README.md +31 -0
- package/examples/quickstart/src/Api.ts +5 -0
- package/examples/quickstart/src/client.ts +13 -0
- package/examples/quickstart/src/greeting/Greeting.ts +12 -0
- package/examples/quickstart/src/greeting/handlers.ts +6 -0
- package/examples/quickstart/src/routes.ts +11 -0
- package/examples/quickstart/src/server.ts +16 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
- package/package.json +32 -14
- package/dist/internal/actions.d.mts +0 -9
- package/dist/internal/authentication.mjs +0 -11
- package/dist/internal/call.mjs +0 -12
- package/dist/internal/cli.mjs +0 -328
- package/dist/internal/client.mjs +0 -25
- package/dist/internal/console.mjs +0 -77
- package/dist/internal/declared.d.mts +0 -13
- package/dist/internal/declared.mjs +0 -34
- package/dist/internal/mcp.d.mts +0 -11
- package/dist/internal/mcp.mjs +0 -52
- package/dist/internal/refusal.mjs +0 -67
- package/dist/internal/schema-errors.mjs +0 -20
- package/dist/internal/tools.mjs +0 -44
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Clock, Console, Effect, Function, Predicate } from "effect";
|
|
2
|
+
//#region src/stdio/console.ts
|
|
3
|
+
const stderrConsole = (console, clock) => {
|
|
4
|
+
const counts = /* @__PURE__ */ new Map();
|
|
5
|
+
const timers = /* @__PURE__ */ new Map();
|
|
6
|
+
const write = (...args) => console.error(...args);
|
|
7
|
+
const elapsed = (label, data) => {
|
|
8
|
+
const start = timers.get(label);
|
|
9
|
+
if (start !== void 0) {
|
|
10
|
+
const millis = Number(clock.monotonicTimeNanosUnsafe() - start) / 1e6;
|
|
11
|
+
write("%s: %sms", label, Number(millis.toFixed(3)), ...data);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const writeLabelKeepingFormat = (label, args) => {
|
|
15
|
+
const [formatOrFirst, ...rest] = args;
|
|
16
|
+
write(...Predicate.isString(formatOrFirst) ? [`${label}: ${formatOrFirst}`, ...rest] : [label, ...args]);
|
|
17
|
+
};
|
|
18
|
+
const group = (...labelOrUndefined) => {
|
|
19
|
+
if (labelOrUndefined.some((part) => part !== void 0)) write(...labelOrUndefined);
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
assert: (condition, ...args) => {
|
|
23
|
+
if (!condition) writeLabelKeepingFormat("Assertion failed", args);
|
|
24
|
+
},
|
|
25
|
+
clear: Function.constVoid,
|
|
26
|
+
count: (label = "default") => {
|
|
27
|
+
const count = (counts.get(label) ?? 0) + 1;
|
|
28
|
+
counts.set(label, count);
|
|
29
|
+
write("%s: %d", label, count);
|
|
30
|
+
},
|
|
31
|
+
countReset: (label = "default") => {
|
|
32
|
+
counts.delete(label);
|
|
33
|
+
},
|
|
34
|
+
debug: write,
|
|
35
|
+
dir: (item) => write("%O", item),
|
|
36
|
+
dirxml: write,
|
|
37
|
+
error: write,
|
|
38
|
+
group,
|
|
39
|
+
groupCollapsed: group,
|
|
40
|
+
groupEnd: Function.constVoid,
|
|
41
|
+
info: write,
|
|
42
|
+
log: write,
|
|
43
|
+
table: (data) => write(data),
|
|
44
|
+
time: (label = "default") => {
|
|
45
|
+
if (!timers.has(label)) timers.set(label, clock.monotonicTimeNanosUnsafe());
|
|
46
|
+
},
|
|
47
|
+
timeEnd: (label = "default") => {
|
|
48
|
+
elapsed(label, []);
|
|
49
|
+
timers.delete(label);
|
|
50
|
+
},
|
|
51
|
+
timeLog: (label = "default", ...data) => elapsed(label, data),
|
|
52
|
+
trace: (...args) => {
|
|
53
|
+
const { stack = "" } = /* @__PURE__ */ new Error();
|
|
54
|
+
const [_header, _traceFrame, ...callerFrames] = stack.split("\n");
|
|
55
|
+
const callerStack = callerFrames.join("\n");
|
|
56
|
+
writeLabelKeepingFormat("Trace", args);
|
|
57
|
+
if (callerStack !== "") write(callerStack);
|
|
58
|
+
},
|
|
59
|
+
warn: write
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const withStderrConsole = (effect) => Effect.provideServiceEffect(effect, Console.Console, Effect.zipWith(Effect.service(Console.Console), Effect.service(Clock.Clock), stderrConsole));
|
|
63
|
+
//#endregion
|
|
64
|
+
export { withStderrConsole };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Layer } from "effect";
|
|
2
2
|
import { HttpClient, HttpRouter, HttpServer } from "effect/http";
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/testing/memory.d.ts
|
|
4
4
|
/** What in-memory routes may leave to the host: the router, the platform, nothing per request. */
|
|
5
5
|
export type Served = HttpRouter.HttpRouter | HttpRouter.Request<"Requires", never> | HttpRouter.Request<"GlobalRequires", never> | HttpRouter.Request<"Error", any> | HttpRouter.Request<"GlobalError", any> | Layer.Success<typeof HttpServer.layerServices>;
|
|
6
6
|
//#endregion
|
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
import { Effect, Layer, Predicate, Stream } from "effect";
|
|
2
2
|
import { FetchHttpClient, HttpBody, HttpClient, HttpClientRequest } from "effect/http";
|
|
3
|
-
//#region src/
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
/**
|
|
7
|
-
* `request`, a streamed body turned into a web stream in the current context. The native client
|
|
8
|
-
* turns it in its send, under the in-memory `fetch`, where another client the stream calls
|
|
9
|
-
* would send to the routes too.
|
|
10
|
-
*/
|
|
11
|
-
const pulled = (request) => {
|
|
3
|
+
//#region src/testing/memory.ts
|
|
4
|
+
const relativeUrlOrigin = "http://localhost/";
|
|
5
|
+
const withWebStreamBodyInCurrentContext = (request) => {
|
|
12
6
|
const body = request.body;
|
|
13
7
|
if (!Predicate.isTagged(body, "Stream")) return Effect.succeed(request);
|
|
14
8
|
return Effect.map(Stream.toReadableStreamEffect(body.stream), (readable) => HttpClientRequest.setBody(request, HttpBody.raw(readable, {
|
|
@@ -16,12 +10,6 @@ const pulled = (request) => {
|
|
|
16
10
|
contentLength: body.contentLength
|
|
17
11
|
})).pipe(HttpClientRequest.updateHeaders(() => request.headers)));
|
|
18
12
|
};
|
|
19
|
-
/**
|
|
20
|
-
* The native `HttpClient`, sending every request to `handler` instead of the network. A
|
|
21
|
-
* relative URL resolves against `http://localhost/`, as a page there would resolve it, and a
|
|
22
|
-
* request carries the `Host` header of its URL, as one over the network does. The client is
|
|
23
|
-
* its own: the program's other clients keep their `fetch`, and get none of its requests.
|
|
24
|
-
*/
|
|
25
13
|
const clientOf = (handler) => {
|
|
26
14
|
const fetch = (input, init) => {
|
|
27
15
|
const request = new Request(input, init);
|
|
@@ -29,9 +17,9 @@ const clientOf = (handler) => {
|
|
|
29
17
|
return handler(request);
|
|
30
18
|
};
|
|
31
19
|
return Layer.effect(HttpClient.HttpClient, Effect.map(HttpClient.HttpClient, (native) => HttpClient.makeWith((request) => Effect.flatMap(request, (mapped) => {
|
|
32
|
-
const resolved = URL.canParse(mapped.url) ? null : URL.parse(mapped.url,
|
|
20
|
+
const resolved = URL.canParse(mapped.url) ? null : URL.parse(mapped.url, relativeUrlOrigin);
|
|
33
21
|
const sent = resolved === null ? mapped : HttpClientRequest.setUrl(mapped, resolved.href);
|
|
34
|
-
return Effect.flatMap(
|
|
22
|
+
return Effect.flatMap(withWebStreamBodyInCurrentContext(sent), (ready) => native.postprocess(Effect.succeed(ready)).pipe(Effect.provideService(FetchHttpClient.Fetch, fetch)));
|
|
35
23
|
}), native.preprocess))).pipe(Layer.provide(Layer.fresh(FetchHttpClient.layer)));
|
|
36
24
|
};
|
|
37
25
|
//#endregion
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { assertOnce, projectedErrors } from "../contract/rules.mjs";
|
|
2
|
+
import { acquire } from "../contract/implementation.mjs";
|
|
3
|
+
import { Effect, Schema } from "effect";
|
|
4
|
+
import { Tool, Toolkit } from "effect/ai";
|
|
5
|
+
//#region src/toolkit/tools.ts
|
|
6
|
+
const annotateIfGiven = (tool, key, value) => value === void 0 ? tool : tool.annotate(key, value);
|
|
7
|
+
const annotateFromContract = (tool, { readOnly, mcp }) => {
|
|
8
|
+
const destructiveUnlessReadOnly = mcp.destructiveHint ?? (readOnly ? false : void 0);
|
|
9
|
+
const meta = mcp._meta === void 0 ? void 0 : { ...mcp._meta };
|
|
10
|
+
const hinted = annotateIfGiven(annotateIfGiven(annotateIfGiven(tool.annotate(Tool.Readonly, readOnly), Tool.Destructive, destructiveUnlessReadOnly), Tool.Idempotent, mcp.idempotentHint), Tool.OpenWorld, mcp.openWorldHint);
|
|
11
|
+
return annotateIfGiven(annotateIfGiven(hinted, Tool.Title, mcp.title), Tool.Meta, meta);
|
|
12
|
+
};
|
|
13
|
+
const jsonToolOf = (action) => Tool.make(action.name, {
|
|
14
|
+
description: action.description,
|
|
15
|
+
parameters: Schema.toCodecJson(action.input),
|
|
16
|
+
success: Schema.toCodecJson(action.success),
|
|
17
|
+
failure: Schema.toCodecJson(Schema.Union(projectedErrors(action))),
|
|
18
|
+
failureMode: "return"
|
|
19
|
+
});
|
|
20
|
+
const bindTools = (apps, projection) => {
|
|
21
|
+
assertOnce(projection.label, apps.flatMap((app) => app.actions));
|
|
22
|
+
const toolkit = Toolkit.make(...apps.flatMap((app) => app.actions.map((action) => annotateFromContract(projection.tool(jsonToolOf(action), action, app), action))));
|
|
23
|
+
return {
|
|
24
|
+
toolkit,
|
|
25
|
+
layer: toolkit.toLayer(Effect.map(acquire(apps), (bound) => Object.fromEntries(bound.map(([action, run]) => [action.name, projection.handler?.(run, action) ?? run]))))
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { bindTools };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Action failure modes
|
|
2
|
+
|
|
3
|
+
What you see when something is wrong, and the fix. Search for a fragment of the message.
|
|
4
|
+
The tasks are in [Action.md](Action.md).
|
|
5
|
+
|
|
6
|
+
## At `make`
|
|
7
|
+
|
|
8
|
+
- `Argument of type 'Struct<…>' is not assignable to parameter of type 'Fields'` at `Schema.Struct(X.input)`, or `Objects property names must be strings` or `Invalid value used as weak map key` thrown: an action's `input` is a schema already, a `Schema.Struct` where fields were given. Use it as it is: `Schema.Array(X.input)`.
|
|
9
|
+
- `Invalid action name: <name>` thrown: an action's name matches `[A-Za-z0-9_-]{1,128}` and is not `then`. Rename the action.
|
|
10
|
+
- `'readOnlyHint' does not exist in type 'Mcp'`, or `Type 'true' is not assignable to type 'never'` on `readOnlyHint` beside another hint: a tool's read-only hint is its action's `readOnly`. Set `readOnly` instead.
|
|
11
|
+
- `'<key>' does not exist in type 'Options'`, or `in type 'Mcp'`, or `Type '...' is not assignable to type 'never'` on the key: `make` takes no such option or hint. The key may be misspelled, such as `destructive` for `destructiveHint`.
|
|
12
|
+
- `Type 'H' is not assignable to type 'H & ...'`, or `… is not assignable to type 'ServiceCaller<…>'`, or `… is not assignable to type '[Extract<ErrorMembers<E>, …>] extends …'`, at `make` in a helper whose type parameter stands for `mcp`, `caller` or `error`, as in `<const H extends Action.Mcp>(mcp: H)`: `make` refuses options a type parameter stands for. Type the parameter concretely, `Action.Mcp` or `typeof CurrentUser`, or spread a constant object into the options.
|
|
13
|
+
- A type error on `error`, `... is not assignable to type 'readonly never[]'` or naming the remaining errors, or `Action "<name>": error _tag "Forbidden" is built in, and declared on every surface` thrown: an action's `error` lists a built-in error, or one of your own that encodes with the `_tag` `InvalidInput`, `Unauthenticated` or `Forbidden`. Drop the built-in error, which a handler may fail with anyway; rename your own.
|
|
14
|
+
- `ReferenceError` thrown where a contract is defined: `make` reads every declared error's `_tag` when it is called, so an entry of `error` that is a `Schema.suspend` whose thunk reads a `const` declared further down fails at once. Declare the error before the action.
|
|
15
|
+
|
|
16
|
+
## At `implement`
|
|
17
|
+
|
|
18
|
+
- `... is not assignable to type '"A protected action takes { authorize }: your rule, or Action.allowAll"'`, or `Protected actions require authorize, or Action.allowAll` thrown: an implementation holding a protected action states `authorize`. Pass `{ authorize }`, or `{ authorize: Action.allowAll }`.
|
|
19
|
+
- `Missing authorize: pass an authorization function, or Action.allowAll` thrown, or a layer build dying with it: `authorize` is neither a function nor an Effect building one, or that Effect built something else.
|
|
20
|
+
- `Type 'undefined' is not assignable to type 'Authorizer<...>'`: `authorize` is never `undefined`. Write `enabled ? authorize : Action.allowAll`.
|
|
21
|
+
- `Parameter 'action' implicitly has an 'any' type`, or an `Effect.fn` authorizer whose `action` is `any`: an authorizer defined apart from `implement` annotates its parameter, `(action: Action.Any)`.
|
|
22
|
+
- `Type 'X' is not assignable to type 'Forbidden | Unauthenticated'`, ending `Property '"~declared in the action's error, or built in"' is missing in type 'X'`: an authorizer fails only with `Action.Unauthenticated` or `Action.Forbidden`, and this one fails with `X`. A limit, or another failure callers should see, is the handler's: declare `X` in the action's `error` and fail with it there.
|
|
23
|
+
- `Argument of type ... is not assignable to parameter of type ...`, or `Type 'Effect<...>' is not assignable to type 'Effect<...>'` at a handler of a record written in place, ending `Property '"~declared in the action's error, or built in"' is missing in type 'X'`, under `Type 'X' is not assignable to type 'Forbidden | InvalidInput | Unauthenticated'` or a longer union: a handler fails only with the errors its action declares and the library's built-in ones, and this one fails with `X`, which may be Effect's `HttpApiError.Forbidden` or a `Forbidden` of your own. Add `X` to the action's `error`, or handle it. A builder's error is reported at the builder and names one handler, in `The types returned by '<handler>(...)'`: the next shows once that one is fixed.
|
|
24
|
+
- `Binding element '<field>' implicitly has an 'any' type` (TS7031), or `Parameter 'input' implicitly has an 'any' type`, at every handler of a builder: the builder is wrapped, as in `Effect.gen(…).pipe(Effect.provide(layer))`, and its record is no longer typed from the contracts. Give `implement` the builder itself, and provide the layer to the surface's layer or to the server.
|
|
25
|
+
- `Missing handlers: <names>` or `Unknown handlers: <keys>` thrown, also when a builder's layer builds: a record of handlers has one own function for each action of its `implement` call, and no other key. Add a missing handler to the record itself, not to a prototype; remove an unknown one, or add its action to the list.
|
|
26
|
+
- `'<name>' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer` (TS7022), or, with the types bypassed, an acquisition that never completes: a builder acquires an `Action.client` of its own implementation, directly or through another builder. Call the services both use instead.
|
|
27
|
+
|
|
28
|
+
## Where an implementation is served
|
|
29
|
+
|
|
30
|
+
- `Type 'CurrentUser' is not assignable to type 'never'`, or `Request<"Requires", CurrentUser>` still owed, for a layer serving only public actions: no surface provides a public action an identity, and a public action's handler reads one. Make the action protected.
|
|
31
|
+
- `Type '{ readonly "A builder yields the caller, which a server has per request: yield it in a handler or an authorizer": { readonly caller: CurrentUser; readonly builderOf: "createNote" | ...; }; }' is not assignable to type 'never'` where a server is launched, or `is missing the following properties from type 'Scope'` at each `it.effect` that runs the routes, one error for each, all of one cause, the builder of the `implement` call whose actions `builderOf` names, or a service shared across requests that should be resolved per request: what a builder, or the Effect building an authorizer, yields is resolved once, at startup, and a server has the caller per request alone. Move the `yield*` into the handler or the authorizer.
|
|
32
|
+
- `HttpRouter.Request<"Requires", X>` still owed, or `Type 'X' is not assignable to type 'never'` at `runMain`, although `X`, a store the authorizer reads, is provided at startup: what a plain authorizer yields is a request-time requirement, and this one yields `X` on every call. Make `authorize` an Effect that yields `X` once and returns the function ([Authorize calls](Action.md#authorize-calls)).
|
|
33
|
+
- `unknown` among a surface's requirements, and a type error where it is served or run, such as `Argument of type 'Layer<never, unknown, unknown>' is not assignable` at `HttpRouter.toWebHandler` or `Type 'unknown' is not assignable to type 'never'`: the implementations are typed `Action.AnyImplementation`, as a helper's parameter or a list's annotation. Make the helper generic over them, `<const Apps extends Action.AnyImplementation | ReadonlyArray<Action.AnyImplementation>>(apps: Apps)`, or drop the annotation.
|
|
34
|
+
- `Request<"Requires", unknown>` where an implementation is served, or `unknown` among the services a call or a run still requires: TypeScript inferred no requirements for one handler of a record, as a data-first call whose type takes a parameter from no argument causes, `Effect.catchTag(effect, tag, f)` for one. Write that handler's Effect `effect.pipe(Effect.catchTag(tag, f))`, or as a generator.
|
|
35
|
+
- A builder runs twice, or a job, an agent or a second server reads other state than the routes, such as an empty in-memory store: inside one `HttpRouter.serve` or one `Testing.layer` a builder runs once, however many surfaces serve it, but each builds its routes in a layer graph of its own, which shares nothing it builds with a layer merged beside it. A job beside the server shares its build only when the job's side builds first, so it can pass in a test and build twice when the job starts later. Put the job or agent inside the layer the server serves, or provide `Action.layer(implementations)` and the services they share above both ([Share one build across servers](Action-in-process.md#share-one-build-across-servers)).
|
|
36
|
+
- Two surfaces in one layer graph, each given startup services of its own, both answer with one surface's: a builder runs once per layer graph, with the services of its first build. Provide the services once; where the surfaces must differ, wrap each in `Layer.fresh`, providing what they share, authentication included, outside it, or give each an implementation of its own.
|
|
37
|
+
- `Not an implementation made by this Action.implement: is effect-actions installed twice?` thrown by a surface, `client` or `layer`: another installed copy of the package made the implementation, or the value is a copy of one, such as a spread. Install one copy of the package, and pass the value `implement` returned.
|
|
38
|
+
|
|
39
|
+
## At `client` and its calls
|
|
40
|
+
|
|
41
|
+
- `Listed in actions, but no implementation holds it: <names>` thrown by `Action.client`: an action listed in `actions` is none the given implementations hold. Pass the implementation holding it, or list the contract value it implemented.
|
|
42
|
+
- `Duplicate action: <name>` thrown: a list given to `implement` or `byName` names one action twice, or two implementations given to `Action.client` implement an action of that name. Two contracts sharing a name are told apart by `actions`, listing one; two implementations of one contract are not: pass one.
|
|
43
|
+
- `Type 'CurrentUser' is not assignable to type 'never'` where a program calling an `Action.client` runs, or `Type 'CurrentUser' is missing the following properties from type 'Scope'` at `it.effect`: a call to a protected action reads the caller from its own context, never from the one the client was acquired in. Provide the caller around the call.
|
|
44
|
+
- `Type 'Scope' is not assignable to type 'never'` where a program runs, or `Service not found: effect/Scope` in plain JavaScript: acquiring an `Action.client` builds, as a layer does. Acquire it in a builder or a layer, or run the program in `Effect.scoped`.
|
|
45
|
+
- A builder runs on every request, or a route owes `HttpRouter.Request<"Requires", X>` for `X`, a service its builder yields: acquiring an `Action.client` runs the builders, and a handler acquires one on every request. Acquire the client in the builder, and call it in the handler.
|
|
46
|
+
- An `Action.client` call dies with a `SchemaError`: the handler's success, or what it fails with, does not pass through its codec. A failure follows the `SchemaError` in the cause, as `Cause.pretty` shows. Return what the success schema accepts, and fail with an error the action declares.
|
|
47
|
+
- `InvalidInput` with `Expected <Class>` from an `Action.client` call given a plain object: an input that is a class encodes only its instances. Pass `new Class({ ... })`.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Action in process
|
|
2
|
+
|
|
3
|
+
Call actions from your own code, such as a job or a test, and share one build of the
|
|
4
|
+
implementations between servers and jobs.
|
|
5
|
+
|
|
6
|
+
Read [Action.md](Action.md) first, and this page alone, in one tool call. When something fails,
|
|
7
|
+
search [Action-failures.md](Action-failures.md). Reading order: [README.md](README.md).
|
|
8
|
+
|
|
9
|
+
## API
|
|
10
|
+
|
|
11
|
+
Import `@gjermundgaraba/effect-actions/Action`. Its other exports are in [Action.md](Action.md#api).
|
|
12
|
+
|
|
13
|
+
| Export | Purpose |
|
|
14
|
+
| --------------------------------------- | ------------------------------------------------------------------------ |
|
|
15
|
+
| `client(implementations, { actions? })` | An Effect of a client running them in process: `client.<action>(input)`. |
|
|
16
|
+
| `layer(implementations)` | Their builders as one layer: above several servers, each runs once. |
|
|
17
|
+
| `Client<Apps>` | What `client` gives for the implementations `Apps`, one or a list. |
|
|
18
|
+
| `ClientOptions` | What `client` takes: `actions`, the ones to call. |
|
|
19
|
+
|
|
20
|
+
## Call actions in process
|
|
21
|
+
|
|
22
|
+
`Action.client(implementations)` gives the methods `ActionHttp.client` gives. A call runs what a
|
|
23
|
+
remote call runs, without a transport: the input and the result pass through their JSON codecs,
|
|
24
|
+
and the authorizer runs before the handler.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const as = (user: keyof typeof users) => Effect.provideService(CurrentUser, users[user]);
|
|
28
|
+
|
|
29
|
+
const program = Effect.gen(function* () {
|
|
30
|
+
const client = yield* Action.client(notes);
|
|
31
|
+
|
|
32
|
+
return yield* client.createNote({ title: "Plan", body: "Draft" }).pipe(as("ada"));
|
|
33
|
+
}).pipe(Effect.scoped, Effect.provide(NoteStore.layerMemory));
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Acquiring a client needs a `Scope`: under `it.effect` the test supplies it, as in
|
|
37
|
+
[notes-service/test/notes/notes.test.ts](../examples/notes-service/test/notes/notes.test.ts);
|
|
38
|
+
elsewhere `Effect.scoped` does, as in [kitchen-sink/src/agent.ts](../examples/kitchen-sink/src/agent.ts).
|
|
39
|
+
|
|
40
|
+
- **Provide the caller around each call, and startup services around the acquisition.** A call
|
|
41
|
+
reads its own context, never the one the client was acquired in, so one client serves every
|
|
42
|
+
caller ([assistant/test/identity/identity.test.ts](../examples/assistant/test/identity/identity.test.ts)).
|
|
43
|
+
- **Acquiring a client runs the builders**, and builds a built authorizer: it owes their startup
|
|
44
|
+
services. Acquire it once, in a builder, a layer, or a scoped program such as a test. Never in
|
|
45
|
+
a handler, which builds on every request.
|
|
46
|
+
- **Two acquisitions share a build only inside one layer graph**, such as under a layer provided
|
|
47
|
+
around the program. With none around them, each builds its own.
|
|
48
|
+
- **Every implemented action has a method**, whether or not a surface serves it. `actions` lists
|
|
49
|
+
the ones to call, and an implementation holding none of them is left unbuilt
|
|
50
|
+
([kitchen-sink/test/issues/issues.test.ts](../examples/kitchen-sink/test/issues/issues.test.ts)).
|
|
51
|
+
- **Test what an implementation does here**, and what a surface adds, its authentication,
|
|
52
|
+
statuses and codecs, in memory ([Testing.md](Testing.md#test-routes-in-memory)). Assert on a
|
|
53
|
+
failure as [Testing.md](Testing.md#assert-on-errors) says.
|
|
54
|
+
|
|
55
|
+
## Share one build across servers
|
|
56
|
+
|
|
57
|
+
Inside one `HttpRouter.serve` or one `Testing.layer`, a builder runs once, however many surfaces
|
|
58
|
+
serve it, a toolkit's `layer` provided to the routes included. Each builds its routes in a layer graph of its own, as `HttpRouter.provideRequest(layer)` builds its layer, which reuses what is provided from
|
|
59
|
+
outside it and shares nothing it builds itself: a builder's run, a built authorizer, or a
|
|
60
|
+
service of yours such as an in-memory store.
|
|
61
|
+
|
|
62
|
+
- **With one server, put everything the process runs inside the layer it serves**, a job or an
|
|
63
|
+
agent beside the routes included.
|
|
64
|
+
- **Otherwise, provide `Action.layer(implementations)` and the services they share above the
|
|
65
|
+
server and its siblings**: a second server, a program beside it, several `Testing.layer`s.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// kitchen-sink/src/routes.ts
|
|
69
|
+
export const builders = Action.layer([status, identity, issues, attachments, admin]);
|
|
70
|
+
|
|
71
|
+
// kitchen-sink/src/server.ts
|
|
72
|
+
Layer.mergeAll(
|
|
73
|
+
HttpRouter.serve(publicRoutes).pipe(Layer.provide(listen(publicPort))),
|
|
74
|
+
HttpRouter.serve(adminRoutes).pipe(Layer.provide(listen(adminPort))),
|
|
75
|
+
).pipe(Layer.provide(builders), Layer.provide(memory));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
That is [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts) and
|
|
79
|
+
[kitchen-sink/src/server.ts](../examples/kitchen-sink/src/server.ts), two ports;
|
|
80
|
+
[kitchen-sink/test/routes.test.ts](../examples/kitchen-sink/test/routes.test.ts) does it with two
|
|
81
|
+
`Testing.layer`s, asserting one run of a builder both ports serve.
|
|
82
|
+
|
|
83
|
+
- **A job merged beside `HttpRouter.serve`, with no `Action.layer` above both, shares the
|
|
84
|
+
server's build only when its own side builds first.** Built after the routes, it builds
|
|
85
|
+
again, with state of its own. Which builds first depends on timing: it can pass in a test and
|
|
86
|
+
build twice once the job starts later.
|
|
87
|
+
- **`Action.layer` builds every implementation it is given**, those no surface serves included,
|
|
88
|
+
so it owes their startup services.
|
|
89
|
+
- **Inside one layer graph, a builder runs with the services of whichever surface builds it
|
|
90
|
+
first**, and services provided around one surface reach the others. Provide startup services
|
|
91
|
+
once, for every surface.
|
|
92
|
+
- **Do not wrap a server in `Layer.fresh`**: two servers already share no build. It is only for
|
|
93
|
+
two surfaces in one graph that must each have a build and startup services of their own.
|