@nice-code/action 0.2.8 → 0.2.10

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 CHANGED
@@ -1 +1,273 @@
1
- # @nice-code/action-test
1
+ # @nice-code/action
2
+
3
+ Typed, transport-agnostic action system for calling functions across client/server boundaries with full TypeScript inference.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @nice-code/action
9
+ ```
10
+
11
+ Peer deps: `valibot` (or any [Standard Schema](https://github.com/standard-schema/standard-schema) library), `@tanstack/react-query` (for `@nice-code/action/react-query`).
12
+
13
+ ## Core concepts
14
+
15
+ - **ActionDomain** — a named group of typed actions (like an API surface)
16
+ - **ActionSchema** — input/output schema + declared error types for one action
17
+ - **ActionRuntime** — processes incoming requests and dispatches them to handlers
18
+ - **ActionLocalHandler** — executes actions in the current process
19
+ - **ActionExternalClientHandler** — forwards/receives actions over HTTP or WebSocket
20
+ - **RuntimeCoordinate** — identifies an environment (frontend, backend, worker…)
21
+
22
+ ---
23
+
24
+ ## Defining actions
25
+
26
+ ### 1. Create a root domain (shared between client and server)
27
+
28
+ ```ts
29
+ import { createActionRootDomain, actionSchema } from "@nice-code/action";
30
+ import * as v from "valibot";
31
+
32
+ // Root domain — no actions, just a namespace anchor
33
+ export const appRoot = createActionRootDomain({ domain: "app_root" });
34
+
35
+ // Child domain with actions
36
+ export const userDomain = appRoot.createChildDomain({
37
+ domain: "user",
38
+ actions: {
39
+ getUser: actionSchema()
40
+ .input({ schema: v.object({ userId: v.string() }) })
41
+ .output({ schema: v.object({ id: v.string(), name: v.string() }) })
42
+ .throws(err_user, ["not_found"]), // from @nice-code/error
43
+
44
+ updateName: actionSchema()
45
+ .input({ schema: v.object({ userId: v.string(), name: v.string() }) })
46
+ .output({ schema: v.object({ success: v.boolean() }) }),
47
+ },
48
+ });
49
+ ```
50
+
51
+ ### 2. Serialization for non-JSON-native types
52
+
53
+ ```ts
54
+ createAt: actionSchema()
55
+ .output(
56
+ { schema: v.object({ createdAt: v.date() }) },
57
+ ({ createdAt }) => ({ createdAt: createdAt.toISOString() }), // serialize
58
+ ({ createdAt }) => ({ createdAt: new Date(createdAt) }), // deserialize
59
+ ),
60
+ ```
61
+
62
+ ### 3. Declare thrown errors
63
+
64
+ ```ts
65
+ import { defineNiceError, err } from "@nice-code/error";
66
+
67
+ const err_user = defineNiceError({
68
+ domain: "err_user",
69
+ schema: {
70
+ not_found: err<{ userId: string }>({
71
+ message: ({ userId }) => `User not found: ${userId}`,
72
+ httpStatusCode: 404,
73
+ context: { required: true },
74
+ }),
75
+ },
76
+ } as const);
77
+
78
+ // Attach to an action schema
79
+ actionSchema()
80
+ .throws(err_user) // any id from err_user
81
+ .throws(err_user, ["not_found"]) // only specific ids
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Setting up runtimes
87
+
88
+ ### Server (local handler)
89
+
90
+ ```ts
91
+ import { ActionRuntime, createLocalHandler, RuntimeCoordinate } from "@nice-code/action";
92
+
93
+ // Identify this environment
94
+ export const serverCoord = RuntimeCoordinate.env("backend");
95
+
96
+ // Implement the actions
97
+ const userHandler = createLocalHandler()
98
+ .forDomain(userDomain, async (request) => {
99
+ if (request.id === "getUser") {
100
+ const user = await db.users.find(request.input.userId);
101
+ if (!user) throw err_user.fromId("not_found", { userId: request.input.userId });
102
+ return user;
103
+ }
104
+ });
105
+
106
+ // Or use the map syntax (preferred)
107
+ const userHandler = createLocalHandler().forDomainActionCases(userDomain, {
108
+ getUser: async (req) => {
109
+ const user = await db.users.find(req.input.userId);
110
+ if (!user) throw err_user.fromId("not_found", { userId: req.input.userId });
111
+ return user;
112
+ },
113
+ updateName: async (req) => {
114
+ await db.users.update(req.input.userId, { name: req.input.name });
115
+ return { success: true };
116
+ },
117
+ });
118
+
119
+ // Or wrap an object directly
120
+ const userHandler = userDomain.wrapAsLocalHandler({
121
+ getUser: async ({ userId }) => { /* ... */ },
122
+ updateName: async ({ userId, name }) => { /* ... */ },
123
+ });
124
+
125
+ // Wire up the runtime
126
+ export const serverRuntime = new ActionRuntime(serverCoord)
127
+ .addHandlers([userHandler])
128
+ .apply();
129
+ ```
130
+
131
+ ### Handling incoming requests (HTTP endpoint)
132
+
133
+ ```ts
134
+ // Hono example
135
+ app.post("/resolve_action", async (c) => {
136
+ const wire = await c.req.json();
137
+ const runningAction = await serverRuntime.handleActionPayloadWire(wire);
138
+ const result = await runningAction.waitForResultPayload();
139
+ return c.json(result.toJsonObject());
140
+ });
141
+ ```
142
+
143
+ ### Client (external handler)
144
+
145
+ ```ts
146
+ import {
147
+ ActionRuntime,
148
+ ActionExternalClientHandler,
149
+ RuntimeCoordinate,
150
+ ETransportType,
151
+ ETransportStatus,
152
+ } from "@nice-code/action";
153
+
154
+ export const clientCoord = RuntimeCoordinate.env("frontend");
155
+ export const serverCoord = RuntimeCoordinate.env("backend");
156
+
157
+ const serverHandler = new ActionExternalClientHandler({
158
+ externalClientSpecifier: serverCoord,
159
+ transports: [
160
+ {
161
+ type: ETransportType.http,
162
+ initialize: () => ({
163
+ getTransport: () => ({
164
+ status: ETransportStatus.ready,
165
+ readyData: {
166
+ createRequest: () => ({ url: "https://api.example.com/resolve_action" }),
167
+ },
168
+ }),
169
+ }),
170
+ },
171
+ ],
172
+ }).forDomain(userDomain);
173
+
174
+ export const clientRuntime = new ActionRuntime(clientCoord)
175
+ .addHandlers([serverHandler])
176
+ .apply();
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Calling actions
182
+
183
+ ```ts
184
+ // Create a request payload
185
+ const request = userDomain.action.getUser.request({ userId: "u_123" });
186
+
187
+ // Run it — returns a RunningAction
188
+ const runningAction = await userDomain.runAction(request);
189
+
190
+ // Wait for the result
191
+ const result = await runningAction.waitForResultPayload();
192
+ console.log(result.output); // { id: "u_123", name: "Alice" }
193
+
194
+ // Or run and get the output directly (throws on error)
195
+ const output = await userDomain.action.getUser
196
+ .request({ userId: "u_123" })
197
+ .runToOutput();
198
+ ```
199
+
200
+ ---
201
+
202
+ ## React Query integration
203
+
204
+ ```ts
205
+ import { useActionQuery, useActionMutation } from "@nice-code/action/react-query";
206
+
207
+ // Query
208
+ function UserProfile({ userId }: { userId: string }) {
209
+ const { data } = useActionQuery(
210
+ userDomain.action.getUser,
211
+ { userId },
212
+ { queryKey: ["user", userId] },
213
+ );
214
+ return <div>{data?.name}</div>;
215
+ }
216
+
217
+ // Mutation
218
+ function RenameUser() {
219
+ const { mutate } = useActionMutation(userDomain.action.updateName);
220
+ return <button onClick={() => mutate({ userId: "u_1", name: "Bob" })}>Rename</button>;
221
+ }
222
+ ```
223
+
224
+ ---
225
+
226
+ ## WebSocket transport
227
+
228
+ ```ts
229
+ {
230
+ type: ETransportType.ws,
231
+ initialize: () => ({
232
+ getTransport: () => ({
233
+ status: ETransportStatus.ready,
234
+ readyData: {
235
+ ws: new WebSocket("wss://api.example.com/resolve_action/ws"),
236
+ },
237
+ }),
238
+ }),
239
+ }
240
+ ```
241
+
242
+ Multiple transports can be registered; the runtime picks the best available one (WebSocket preferred for lower latency, HTTP as fallback).
243
+
244
+ ---
245
+
246
+ ## RuntimeCoordinate
247
+
248
+ Identifies a runtime environment and is used to route actions to the right handler.
249
+
250
+ ```ts
251
+ RuntimeCoordinate.env("backend") // named env
252
+ RuntimeCoordinate.env("backend").specify({ perId: "worker-1" }) // env + instance
253
+ RuntimeCoordinate.unknown // unspecified
254
+ ```
255
+
256
+ ---
257
+
258
+ ## Error handling in actions
259
+
260
+ Actions declared with `.throws(domain, ids?)` surface typed errors at the call site:
261
+
262
+ ```ts
263
+ import { castNiceError } from "@nice-code/error";
264
+
265
+ try {
266
+ const output = await userDomain.action.getUser.request({ userId }).runToOutput();
267
+ } catch (e) {
268
+ const error = castNiceError(e);
269
+ if (err_user.isExact(error) && error.hasId("not_found")) {
270
+ console.log("User not found:", error.getContext("not_found").userId);
271
+ }
272
+ }
273
+ ```