@rc312/tss-effect-serverfn 0.0.0-stage → 0.2.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -2
  3. package/dist/bootstrap.server.d.ts +12 -0
  4. package/dist/bootstrap.server.d.ts.map +1 -0
  5. package/dist/bootstrap.server.js +17 -0
  6. package/dist/bootstrap.server.js.map +1 -0
  7. package/dist/boundary.server.d.ts +26 -0
  8. package/dist/boundary.server.d.ts.map +1 -0
  9. package/dist/boundary.server.js +117 -0
  10. package/dist/boundary.server.js.map +1 -0
  11. package/dist/compiler-runtime.d.ts +40 -0
  12. package/dist/compiler-runtime.d.ts.map +1 -0
  13. package/dist/compiler-runtime.js +54 -0
  14. package/dist/compiler-runtime.js.map +1 -0
  15. package/dist/create-effect-server-fn.d.ts +84 -0
  16. package/dist/create-effect-server-fn.d.ts.map +1 -0
  17. package/dist/create-effect-server-fn.js +27 -0
  18. package/dist/create-effect-server-fn.js.map +1 -0
  19. package/dist/effect-middleware.d.ts +116 -0
  20. package/dist/effect-middleware.d.ts.map +1 -0
  21. package/dist/effect-middleware.js +46 -0
  22. package/dist/effect-middleware.js.map +1 -0
  23. package/dist/execution.d.ts +35 -0
  24. package/dist/execution.d.ts.map +1 -0
  25. package/dist/execution.js +7 -0
  26. package/dist/execution.js.map +1 -0
  27. package/dist/failure.server.d.ts +44 -0
  28. package/dist/failure.server.d.ts.map +1 -0
  29. package/dist/failure.server.js +161 -0
  30. package/dist/failure.server.js.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +3 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/provenance.server.d.ts +54 -0
  36. package/dist/provenance.server.d.ts.map +1 -0
  37. package/dist/provenance.server.js +87 -0
  38. package/dist/provenance.server.js.map +1 -0
  39. package/dist/server.d.ts +3 -0
  40. package/dist/server.d.ts.map +1 -0
  41. package/dist/server.js +3 -0
  42. package/dist/server.js.map +1 -0
  43. package/dist/vite.d.ts +24 -0
  44. package/dist/vite.d.ts.map +1 -0
  45. package/dist/vite.js +296 -0
  46. package/dist/vite.js.map +1 -0
  47. package/docs/composition.md +188 -0
  48. package/docs/development.md +48 -0
  49. package/docs/usage.md +223 -0
  50. package/package.json +69 -4
  51. package/src/bootstrap.server.ts +22 -0
  52. package/src/boundary.server.ts +198 -0
  53. package/src/compiler-runtime.ts +75 -0
  54. package/src/create-effect-server-fn.ts +197 -0
  55. package/src/effect-middleware.ts +171 -0
  56. package/src/execution.ts +39 -0
  57. package/src/failure.server.ts +257 -0
  58. package/src/index.ts +12 -0
  59. package/src/provenance.server.ts +125 -0
  60. package/src/server.ts +7 -0
  61. package/src/vite.ts +462 -0
package/dist/vite.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { Plugin } from "vite";
2
+ /**
3
+ * Adapt Effect Schemas and handlers before TanStack Start compiles server functions.
4
+ *
5
+ * Install before `tanstackStart()`. The plugin recognizes static builder chains
6
+ * and transparent aliases/re-exports, wraps validator and handler arguments, and
7
+ * emits source maps. Start remains responsible for extraction, RPC identifiers,
8
+ * manifests, and removal of server implementations from browser bundles.
9
+ *
10
+ * @returns A Vite pre-transform plugin with build/watch module-cache invalidation.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { tanstackStart } from "@tanstack/react-start/plugin/vite";
15
+ * import { effectServerFns } from "@rc312/tss-effect-serverfn/vite";
16
+ * import { defineConfig } from "vite";
17
+ *
18
+ * export default defineConfig({
19
+ * plugins: [effectServerFns(), tanstackStart()],
20
+ * });
21
+ * ```
22
+ */
23
+ export declare const effectServerFns: () => Plugin;
24
+ //# sourceMappingURL=vite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAuWnC;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,eAAe,QAAO,MA6ElC,CAAC"}
package/dist/vite.js ADDED
@@ -0,0 +1,296 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import * as babel from "@babel/core";
3
+ import * as t from "@babel/types";
4
+ const runtimeModule = "@rc312/tss-effect-serverfn/compiler-runtime";
5
+ const builderMethods = new Set(["middleware", "validator", "handler"]);
6
+ const staticName = (node) => {
7
+ if (t.isIdentifier(node)) {
8
+ return node.name;
9
+ }
10
+ return t.isStringLiteral(node) ? node.value : undefined;
11
+ };
12
+ const parseModule = async (code, id) => {
13
+ const ast = await babel.parseAsync(code, {
14
+ babelrc: false,
15
+ configFile: false,
16
+ filename: id,
17
+ parserOpts: { plugins: ["typescript", "jsx"] },
18
+ sourceType: "module",
19
+ });
20
+ let program;
21
+ if (ast) {
22
+ babel.traverse(ast, {
23
+ Program(path) {
24
+ program = path;
25
+ path.stop();
26
+ },
27
+ });
28
+ }
29
+ if (!ast || !program) {
30
+ throw new Error(`Unable to parse Effect server functions in ${id}`);
31
+ }
32
+ return { ast, program };
33
+ };
34
+ /**
35
+ * Resolve whether an expression originates from `createEffectServerFn`.
36
+ * Follows lexical bindings, configured builders, and application re-exports;
37
+ * Babel NodePaths preserve scope so shadowed names are not mistaken for imports.
38
+ * Traversal-local visited keys terminate alias and export cycles.
39
+ *
40
+ * @param load - Load an application module with its binding-aware Program path.
41
+ * @param resolve - Resolve imports through Vite's module-resolution rules.
42
+ * @returns An asynchronous predicate over expressions in a module.
43
+ * @throws {Error} When a recognized factory uses unsupported mutable or dynamic composition.
44
+ */
45
+ const createFactoryResolver = (load, resolve) => {
46
+ const trace = {
47
+ async factory(path, id, seen = new Set()) {
48
+ if (path.isTSAsExpression()) {
49
+ return trace.factory(path.get("expression"), id, seen);
50
+ }
51
+ if (path.isTSSatisfiesExpression()) {
52
+ return trace.factory(path.get("expression"), id, seen);
53
+ }
54
+ if (path.isTSNonNullExpression()) {
55
+ return trace.factory(path.get("expression"), id, seen);
56
+ }
57
+ if (path.isCallExpression()) {
58
+ return trace.factory(path.get("callee"), id, seen);
59
+ }
60
+ if (path.isMemberExpression()) {
61
+ return trace.member(path, id, seen);
62
+ }
63
+ if (path.isImportSpecifier() && path.parentPath.isImportDeclaration()) {
64
+ const { imported } = path.node;
65
+ return trace.imported(path.parentPath.node.source.value, t.isIdentifier(imported) ? imported.name : imported.value, id, seen);
66
+ }
67
+ if (path.isVariableDeclarator()) {
68
+ return trace.factory(path.get("init"), id, seen);
69
+ }
70
+ if (path.isArrowFunctionExpression() &&
71
+ (await trace.factory(path.get("body"), id, seen))) {
72
+ throw new Error(`Effect factory wrappers are not supported in ${id}; export a configured builder directly`);
73
+ }
74
+ if (!path.isIdentifier()) {
75
+ return false;
76
+ }
77
+ const binding = path.scope.getBinding(path.node.name);
78
+ const key = `${id}:${binding?.scope.uid}:${path.node.name}`;
79
+ if (!binding || seen.has(key)) {
80
+ return false;
81
+ }
82
+ const found = await trace.factory(binding.path, id, new Set([...seen, key]));
83
+ if (found && !binding.constant) {
84
+ throw new Error(`Mutable Effect factory binding ${path.node.name} in ${id}; use const`);
85
+ }
86
+ return found;
87
+ },
88
+ async imported(source, name, importer, seen) {
89
+ if (source === "@rc312/tss-effect-serverfn") {
90
+ return name === "createEffectServerFn";
91
+ }
92
+ const id = await resolve(source, importer);
93
+ if (!id ||
94
+ id.includes("/node_modules/") ||
95
+ id.startsWith("\0") ||
96
+ seen.has(`${id}:${name}`)) {
97
+ return false;
98
+ }
99
+ const next = new Set([...seen, `${id}:${name}`]);
100
+ const { program } = await load(id);
101
+ const stars = [];
102
+ for (const statement of program.get("body")) {
103
+ if (statement.isExportAllDeclaration()) {
104
+ stars.push(statement.node.source.value);
105
+ }
106
+ if (!statement.isExportNamedDeclaration()) {
107
+ continue;
108
+ }
109
+ for (const specifier of statement.get("specifiers")) {
110
+ if (specifier.isExportSpecifier() &&
111
+ staticName(specifier.node.exported) === name) {
112
+ return statement.node.source
113
+ ? trace.imported(statement.node.source.value, specifier.node.local.name, id, next)
114
+ : trace.factory(specifier.get("local"), id, next);
115
+ }
116
+ }
117
+ const declaration = statement.get("declaration");
118
+ if (declaration.isVariableDeclaration()) {
119
+ const exported = declaration
120
+ .get("declarations")
121
+ .find((item) => item.get("id").isIdentifier({ name }));
122
+ if (exported) {
123
+ return trace.factory(exported.get("id"), id, next);
124
+ }
125
+ }
126
+ }
127
+ const matches = await Promise.all(stars.map((star) => trace.imported(star, name, id, next)));
128
+ return matches.some(Boolean);
129
+ },
130
+ async member(path, id, seen) {
131
+ const object = path.get("object");
132
+ if (await trace.factory(object, id, seen)) {
133
+ if (path.node.computed ||
134
+ !t.isIdentifier(path.node.property) ||
135
+ !builderMethods.has(path.node.property.name)) {
136
+ throw new Error(`Unsupported Effect builder composition in ${id}; use static .middleware(), .validator(), and .handler() calls`);
137
+ }
138
+ return true;
139
+ }
140
+ const binding = object.isIdentifier()
141
+ ? object.scope.getBinding(object.node.name)
142
+ : undefined;
143
+ if (!path.node.computed &&
144
+ t.isIdentifier(path.node.property) &&
145
+ binding?.path.isImportNamespaceSpecifier() &&
146
+ binding.path.parentPath.isImportDeclaration()) {
147
+ return trace.imported(binding.path.parentPath.node.source.value, path.node.property.name, id, seen);
148
+ }
149
+ return false;
150
+ },
151
+ };
152
+ return trace.factory;
153
+ };
154
+ /** Check helper import identity, rather than its local name, to keep lowering idempotent. */
155
+ const isAlreadyLowered = (argument, helper) => {
156
+ if (!argument.isCallExpression() || !t.isIdentifier(argument.node.callee)) {
157
+ return false;
158
+ }
159
+ const binding = argument.scope.getBinding(argument.node.callee.name);
160
+ return (binding?.path.isImportSpecifier() &&
161
+ t.isIdentifier(binding.path.node.imported, { name: helper }) &&
162
+ binding.path.parentPath.isImportDeclaration() &&
163
+ binding.path.parentPath.node.source.value === runtimeModule);
164
+ };
165
+ /** Collect syntactic candidates; binding analysis must approve them before rewriting. */
166
+ const collectBuilderCalls = (program) => {
167
+ const candidates = [];
168
+ program.traverse({
169
+ CallExpression(path) {
170
+ const { callee } = path.node;
171
+ if (t.isMemberExpression(callee) &&
172
+ ["validator", "handler"].includes(staticName(callee.property) ?? "")) {
173
+ candidates.push(path);
174
+ }
175
+ },
176
+ });
177
+ return candidates;
178
+ };
179
+ /**
180
+ * Wrap validated builder arguments with generated runtime helpers in place.
181
+ * Input paths must already be resolved to Effect builders. Fresh import names
182
+ * avoid lexical collisions; previously lowered arguments remain untouched.
183
+ *
184
+ * @returns Whether any AST nodes and helper imports were added or changed.
185
+ * @throws {Error} For computed methods or calls without exactly one expression argument.
186
+ */
187
+ const lowerBuilderCalls = (calls, program, id) => {
188
+ const helpers = new Map();
189
+ for (const call of calls) {
190
+ const { callee } = call.node;
191
+ if (!t.isMemberExpression(callee) ||
192
+ callee.computed ||
193
+ !t.isIdentifier(callee.property)) {
194
+ throw new Error(`Computed Effect builder methods are unsupported in ${id}`);
195
+ }
196
+ const [argument] = call.get("arguments");
197
+ if (call.node.arguments.length !== 1 || !argument?.isExpression()) {
198
+ throw new Error(`Effect builders require one schema or handler expression before TanStack compilation in ${id}`);
199
+ }
200
+ const helper = callee.property.name === "validator" ? "prepareInput" : "effectHandler";
201
+ if (isAlreadyLowered(argument, helper)) {
202
+ continue;
203
+ }
204
+ let local = helpers.get(helper);
205
+ if (!local) {
206
+ local = program.scope.generateUidIdentifier(helper);
207
+ helpers.set(helper, local);
208
+ }
209
+ argument.replaceWith(t.callExpression(local, [argument.node]));
210
+ }
211
+ if (helpers.size === 0) {
212
+ return false;
213
+ }
214
+ program.unshiftContainer("body", t.importDeclaration(Array.from(helpers, ([name, local]) => t.importSpecifier(local, t.identifier(name))), t.stringLiteral(runtimeModule)));
215
+ return true;
216
+ };
217
+ /**
218
+ * Adapt Effect Schemas and handlers before TanStack Start compiles server functions.
219
+ *
220
+ * Install before `tanstackStart()`. The plugin recognizes static builder chains
221
+ * and transparent aliases/re-exports, wraps validator and handler arguments, and
222
+ * emits source maps. Start remains responsible for extraction, RPC identifiers,
223
+ * manifests, and removal of server implementations from browser bundles.
224
+ *
225
+ * @returns A Vite pre-transform plugin with build/watch module-cache invalidation.
226
+ *
227
+ * @example
228
+ * ```ts
229
+ * import { tanstackStart } from "@tanstack/react-start/plugin/vite";
230
+ * import { effectServerFns } from "@rc312/tss-effect-serverfn/vite";
231
+ * import { defineConfig } from "vite";
232
+ *
233
+ * export default defineConfig({
234
+ * plugins: [effectServerFns(), tanstackStart()],
235
+ * });
236
+ * ```
237
+ */
238
+ export const effectServerFns = () => {
239
+ const modules = new Map();
240
+ const load = (id) => {
241
+ let module = modules.get(id);
242
+ if (!module) {
243
+ module = (async () => parseModule(await readFile(id, "utf-8"), id))();
244
+ modules.set(id, module);
245
+ }
246
+ return module;
247
+ };
248
+ return {
249
+ buildStart() {
250
+ modules.clear();
251
+ },
252
+ enforce: "pre",
253
+ name: "@rc312/tss-effect-serverfn",
254
+ async transform(code, id) {
255
+ if (!/\.[cm]?[jt]sx?(?:\?|$)/u.test(id) ||
256
+ !/(?:validator|handler)/u.test(code)) {
257
+ return;
258
+ }
259
+ const cleanId = id.split("?")[0] ?? id;
260
+ const { ast, program } = await parseModule(code, cleanId);
261
+ const candidates = collectBuilderCalls(program);
262
+ const isFactory = createFactoryResolver(load, async (source, importer) => {
263
+ const resolved = await this.resolve(source, importer, {
264
+ skipSelf: true,
265
+ });
266
+ return resolved?.id;
267
+ });
268
+ // Finish binding analysis before mutating paths shared by nested builder calls.
269
+ const resolvedCalls = await Promise.all(candidates.map(async (candidate) => {
270
+ const callee = candidate.get("callee");
271
+ return callee.isMemberExpression() &&
272
+ (await isFactory(callee.get("object"), cleanId))
273
+ ? candidate
274
+ : undefined;
275
+ }));
276
+ const effectCalls = resolvedCalls.filter((call) => call !== undefined);
277
+ if (!lowerBuilderCalls(effectCalls, program, id)) {
278
+ return;
279
+ }
280
+ const result = await babel.transformFromAstAsync(ast, code, {
281
+ babelrc: false,
282
+ configFile: false,
283
+ filename: cleanId,
284
+ sourceFileName: cleanId,
285
+ sourceMaps: true,
286
+ });
287
+ if (result?.code) {
288
+ return { code: result.code, map: result.map ?? null };
289
+ }
290
+ },
291
+ watchChange() {
292
+ modules.clear();
293
+ },
294
+ };
295
+ };
296
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite.js","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,188 @@
1
+ # Composing Effect handlers
2
+
3
+ Handlers can return any native Effect. `Effect.gen`, `pipe`, `.pipe`, `flow`, `Effect.fn`, and do notation all use the same configured runtime, decoded input, and request Scope.
4
+
5
+ Start with the [configured factory](../README.md#quick-start). Try the examples in the [basic app](../examples/basic): its **Composition** selector runs five equivalent server functions through real RPCs.
6
+
7
+ ## Choose a composition style
8
+
9
+ | API | Use it for |
10
+ | --- | --- |
11
+ | `Effect.gen` | Sequential steps with `yield*`, local variables, and branching |
12
+ | `pipe(value, ...steps)` / `value.pipe(...steps)` | Passing a value through transformations; both forms are equivalent for Effects |
13
+ | `flow(...steps)` | Building a reusable function that composes steps left to right |
14
+ | `Effect.fn("name")(body, ...steps)` | A reusable Effect-returning function with a tracing span and optional result combinators |
15
+ | `Effect.map` | Transforming a success value with a pure function |
16
+ | `Effect.flatMap` | Running a dependent Effect and using its result |
17
+ | `Effect.tap` | Awaiting a dependent Effect while retaining the original success value |
18
+ | `Effect.as` | Replacing a successful result with a constant |
19
+ | `Effect.all` | Combining independent Effects into a tuple or record; concurrency is explicit |
20
+ | `Effect.Do`, `bind`, `let` | Building up named results: `bind` runs Effects, `let` adds pure values |
21
+ | `Effect.catchTag` | Recovering from one tagged business failure |
22
+
23
+ `pipe` and `flow` apply ordinary functions; they do not execute the Effects those functions construct. Keep side effects inside Effect constructors such as `Effect.sync`.
24
+
25
+ ## Pipe a Schema, then pipe the decoded value
26
+
27
+ Here is a complete endpoint module using the configured factory:
28
+
29
+ ```ts
30
+ // src/greeting.functions.ts
31
+ import { Effect, flow, pipe, Schema } from "effect";
32
+
33
+ import { createEffectServerFn } from "./effect-server-fn";
34
+
35
+ const Input = Schema.Struct({
36
+ count: Schema.FiniteFromString.pipe(Schema.check(Schema.isGreaterThan(0))),
37
+ name: Schema.NonEmptyString,
38
+ });
39
+
40
+ const greeting = (name: string) => Effect.succeed(`Hello, ${name}!`);
41
+ const environment = () => Effect.succeed("server");
42
+
43
+ export const getPipedGreeting = createEffectServerFn({ method: "GET" })
44
+ .validator(Input)
45
+ .handler(({ data }) =>
46
+ pipe(
47
+ greeting(data.name),
48
+ Effect.flatMap((message) =>
49
+ environment().pipe(
50
+ Effect.map((value) => ({
51
+ count: data.count,
52
+ environment: value,
53
+ message,
54
+ }))
55
+ )
56
+ ),
57
+ Effect.withSpan("Greeting.pipe")
58
+ )
59
+ );
60
+ ```
61
+
62
+ The browser sends `{ count: "41", name: "Ada" }`. The handler receives a **number** for `data.count`; the positive-number check runs after decoding. A count of `"0"` or an invalid numeric string rejects with `INVALID_INPUT` before the handler runs.
63
+
64
+ The runnable app uses Start's native environment helpers for `greeting` and `environment`; their returned Effects compose in exactly the same way.
65
+
66
+ ## Pass `flow` directly to `.handler`
67
+
68
+ Add this to the same module:
69
+
70
+ ```ts
71
+ const greetTogether = (data: typeof Input.Type) =>
72
+ Effect.all(
73
+ {
74
+ count: Effect.succeed(data.count),
75
+ environment: environment(),
76
+ message: greeting(data.name),
77
+ },
78
+ { concurrency: 2 }
79
+ );
80
+
81
+ export const getFlowGreeting = createEffectServerFn({ method: "GET" })
82
+ .validator(Input)
83
+ .handler(
84
+ flow(
85
+ ({ data }: { data: typeof Input.Type }) => data,
86
+ greetTogether,
87
+ Effect.withSpan("Greeting.flow")
88
+ )
89
+ );
90
+ ```
91
+
92
+ Annotating the first function with `typeof Input.Type` gives a separately composed function its decoded input type. The client's input is still inferred from the Schema's encoded type. Error and required-service types continue through the composition.
93
+
94
+ `Effect.all` preserves the record's keys and bounds concurrency here to two. Use `flatMap`, `tap`, or `yield*` when one operation must finish before another starts, such as saving before reading updated state.
95
+
96
+ ## Pass `Effect.fn` directly to `.handler`
97
+
98
+ The same module can also export:
99
+
100
+ ```ts
101
+ export const getFnGreeting = createEffectServerFn({ method: "GET" })
102
+ .validator(Input)
103
+ .handler(
104
+ Effect.fn("Greeting.fn")(
105
+ function* greet({ data }: { data: typeof Input.Type }) {
106
+ return {
107
+ count: data.count,
108
+ environment: yield* environment(),
109
+ message: yield* greeting(data.name),
110
+ };
111
+ },
112
+ Effect.annotateLogs({ feature: "greeting" })
113
+ )
114
+ );
115
+ ```
116
+
117
+ `Effect.fn` accepts a generator or an Effect-returning callback. Supply its result combinators as additional arguments, as above. When calling an existing Effect-returning function, pipe the returned Effect: `greetTogether(data).pipe(Effect.withSpan("Greeting.request"))`.
118
+
119
+ ## Build a record with do notation
120
+
121
+ Another export in the same module:
122
+
123
+ ```ts
124
+ export const getDoGreeting = createEffectServerFn({ method: "GET" })
125
+ .validator(Input)
126
+ .handler(({ data }) =>
127
+ Effect.Do.pipe(
128
+ Effect.bind("message", () => greeting(data.name)),
129
+ Effect.bind("environment", () => environment()),
130
+ Effect.let("count", () => data.count)
131
+ )
132
+ );
133
+ ```
134
+
135
+ Each step retains earlier fields. A later `bind` or `let` callback can use those fields. Use `Effect.map` at the end if you want to project a different response shape.
136
+
137
+ ## Pipe service work and recover a business error
138
+
139
+ The [services app](../examples/services) provides `Notes` through its application-owned `ManagedRuntime`. Its `notes.functions.ts` composes the service directly:
140
+
141
+ ```ts
142
+ import { Effect, flow, Schema } from "effect";
143
+
144
+ import { createEffectServerFn } from "./effect-server-fn";
145
+ import { Notes } from "./notes.server";
146
+
147
+ const RenameInput = Schema.Struct({
148
+ id: Schema.NonEmptyString,
149
+ title: Schema.NonEmptyString,
150
+ });
151
+
152
+ const saveNote = Effect.fn("Notes.save")((data: typeof RenameInput.Type) =>
153
+ Notes.pipe(
154
+ Effect.tap((notes) => notes.rename(data.id, data.title)),
155
+ Effect.flatMap((notes) => notes.read),
156
+ Effect.map((note) => ({ saved: true, title: note.title }))
157
+ )
158
+ );
159
+
160
+ export const renameNote = createEffectServerFn({ method: "POST" })
161
+ .validator(RenameInput)
162
+ .handler(
163
+ flow(({ data }: { data: typeof RenameInput.Type }) => data, saveNote)
164
+ );
165
+
166
+ export const renameNoteIfPresent = createEffectServerFn({ method: "POST" })
167
+ .validator(RenameInput)
168
+ .handler(({ data }) =>
169
+ saveNote(data).pipe(
170
+ Effect.catchTag("NoteMissing", () =>
171
+ Effect.succeed({ saved: false, title: null })
172
+ )
173
+ )
174
+ );
175
+ ```
176
+
177
+ `tap` awaits the mutation and retains the `Notes` service, so `flatMap` can read the saved title. A failed mutation skips that read. `catchTag` implements an explicit policy: a missing note is a successful skip. In the app, enable **Recover missing notes with catchTag** and submit ID `missing` to try it.
178
+
179
+ Without recovery, `NoteMissing` reaches the configured public-error boundary. With recovery, it leaves the Effect error channel and becomes a normal successful RPC result. Defects still use the safe fallback. Endpoint Schema failures happen before this handler and cannot be caught by its pipeline.
180
+
181
+ ## Execution stays at the boundary
182
+
183
+ - Keep the static `createEffectServerFn(...).validator(...).handler(...)` declaration so Start can extract it. Compose the Schema and handler freely inside that declaration.
184
+ - Return the final Effect from the handler. The configured middleware runs it once; the client keeps Start's Promise API.
185
+ - `Effect.ensuring` finalizers and request-scoped `Effect.acquireRelease` cleanup are awaited before the handler Promise settles. Piping does not change Scope ownership.
186
+ - Compose internal Effect operations such as `saveNote` for reuse between endpoints. An exported server function is a framework/RPC boundary, not an Effect-returning domain function.
187
+
188
+ The browser suite exercises every style in dev and production preview. Type tests check encoded/decoded inputs, success values, typed recovery, and required services; boundary tests check lazy construction, `tap`, `as`, `ensuring`, and scoped cleanup on success and recovery.
@@ -0,0 +1,48 @@
1
+ # Package development
2
+
3
+ Run the following commands from the repository root unless noted otherwise.
4
+
5
+ ## Development
6
+
7
+ Use Bun 1.3.13 and Node.js 22.19 or newer.
8
+
9
+ ```sh
10
+ bun install
11
+ bun run check # lint, builds, typechecks, and package tests
12
+ bun run build # package and example builds through Turbo
13
+ bun run dev # package watcher and both example dev servers
14
+ bun run format # apply formatting and safe lint fixes
15
+ ```
16
+
17
+ The package builds with standard `tsc -b`. Effect and TanStack are peers so applications share the same runtime and framework instances.
18
+
19
+ ## Browser tests
20
+
21
+ ```sh
22
+ (cd packages/tss-effect-serverfn && bunx --no-install playwright install chromium)
23
+ bun run test:e2e
24
+ ```
25
+
26
+ The suite runs both examples against Vite dev and production preview, using real browser interactions and RPC responses. See [E2E coverage and debugging](../e2e/README.md). Failures retain Playwright traces and screenshots in the package directory.
27
+
28
+ ## TypeScript configuration
29
+
30
+ `tsconfig.base.json` holds shared strict compiler settings and Effect diagnostics. Each package extends it with its own source include. The root `tsconfig.json` is a solution config: add a project reference there when adding a package, and add package-level references for workspace dependencies.
31
+
32
+ `bunx tsc -b` builds the package solution. Examples use Vite-oriented TypeScript configs and `noEmit`. `bun run typecheck` builds first so generated route types and package exports exist, then checks the package, both apps, and E2E code. Package builds emit JavaScript, declarations, and maps into `dist/`; the published package includes `src/` so map navigation works.
33
+
34
+ ## Releases
35
+
36
+ The package is published as `@rc312/tss-effect-serverfn` under the MIT license. Nothing is published automatically on push.
37
+
38
+ 1. Run `bun run changeset` and commit the generated changeset with your changes.
39
+ 2. Run `bun run version-packages`, then `bun install` to refresh the lockfile. Commit the version changes, changelog, and lockfile to `main`.
40
+ 3. Inspect the artifact locally:
41
+
42
+ ```sh
43
+ bun run check
44
+ cd packages/tss-effect-serverfn
45
+ npm pack --dry-run
46
+ ```
47
+
48
+ 4. To publish locally, authenticate with `npm login`, return to the root, and run `bun run release`. Changesets publishes only unreleased package versions. Push any generated version tags if you want them recorded on GitHub.