ignotum 0.0.0 → 0.0.2

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 (57) hide show
  1. package/README.md +161 -0
  2. package/dist/cli/bin.d.mts +1 -0
  3. package/dist/cli/bin.mjs +1696 -0
  4. package/dist/cli/bin.mjs.map +1 -0
  5. package/dist/runtime/api-BXXIZc_Q.js +119 -0
  6. package/dist/runtime/api-BXXIZc_Q.js.map +1 -0
  7. package/dist/runtime/api-Cpx57mk3.d.ts +47 -0
  8. package/dist/runtime/client/jsx-dev-runtime.d.ts +2 -0
  9. package/dist/runtime/client/jsx-dev-runtime.js +2 -0
  10. package/dist/runtime/client/jsx-runtime.d.ts +2 -0
  11. package/dist/runtime/client/jsx-runtime.js +2 -0
  12. package/dist/runtime/client.d.ts +29 -0
  13. package/dist/runtime/client.js +364 -0
  14. package/dist/runtime/client.js.map +1 -0
  15. package/dist/runtime/index-BgWROoyk.d.ts +249 -0
  16. package/dist/runtime/internal/api.d.ts +2 -0
  17. package/dist/runtime/internal/api.js +2 -0
  18. package/dist/runtime/internal/server.d.ts +6 -0
  19. package/dist/runtime/internal/server.js +7 -0
  20. package/dist/runtime/internal/server.js.map +1 -0
  21. package/dist/runtime/internal/types.d.ts +2 -0
  22. package/dist/runtime/internal/types.js +2 -0
  23. package/dist/runtime/result-B2W-z2wG.js +136 -0
  24. package/dist/runtime/result-B2W-z2wG.js.map +1 -0
  25. package/dist/runtime/result-C1ZdsM6Y.d.ts +106 -0
  26. package/dist/runtime/schema-CNEVLF7D.js +116 -0
  27. package/dist/runtime/schema-CNEVLF7D.js.map +1 -0
  28. package/dist/runtime/server.d.ts +9 -0
  29. package/dist/runtime/server.js +9 -0
  30. package/dist/runtime/server.js.map +1 -0
  31. package/package.json +81 -2
  32. package/src/cli/agent-files.ts +35 -0
  33. package/src/cli/bin.ts +5 -0
  34. package/src/cli/client-plugin.ts +66 -0
  35. package/src/cli/codegen.ts +203 -0
  36. package/src/cli/command.ts +155 -0
  37. package/src/cli/dev.ts +206 -0
  38. package/src/cli/new-project.ts +377 -0
  39. package/src/cli/package-manager.ts +55 -0
  40. package/src/client/errors.ts +83 -0
  41. package/src/client/hooks.ts +141 -0
  42. package/src/client/index.ts +90 -0
  43. package/src/client/jsx-dev-runtime.ts +2 -0
  44. package/src/client/jsx-runtime.ts +2 -0
  45. package/src/client/query.ts +17 -0
  46. package/src/client/sync.ts +487 -0
  47. package/src/dev-runtime/database.ts +374 -0
  48. package/src/dev-runtime/dev-database.ts +199 -0
  49. package/src/dev-runtime/functions.ts +293 -0
  50. package/src/dev-runtime/id.ts +11 -0
  51. package/src/dev-runtime/sync.ts +473 -0
  52. package/src/internal/api.ts +141 -0
  53. package/src/internal/http-paths.ts +5 -0
  54. package/src/internal/server.ts +3 -0
  55. package/src/internal/types.ts +2 -0
  56. package/src/raw.d.ts +4 -0
  57. package/src/server/index.ts +8 -0
@@ -0,0 +1,377 @@
1
+ import { Effect, FileSystem, Path, Schema, String } from "effect";
2
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
3
+
4
+ import { agentProjectFiles, type ProjectFile } from "./agent-files.js";
5
+ import { generate } from "./codegen.js";
6
+ import { installDependencies, type PackageManager } from "./package-manager.js";
7
+
8
+ export interface NewProjectOptions {
9
+ readonly currentDirectory: string;
10
+ readonly directory: string;
11
+ readonly git: boolean;
12
+ readonly install: boolean;
13
+ }
14
+
15
+ export interface NewProjectResult {
16
+ readonly directory: string;
17
+ readonly gitInitialized: boolean;
18
+ readonly packageManager: PackageManager | null;
19
+ readonly projectName: string;
20
+ }
21
+
22
+ export class ProjectDirectoryNotEmpty extends Schema.TaggedError<ProjectDirectoryNotEmpty>()(
23
+ "ProjectDirectoryNotEmpty",
24
+ {
25
+ message: Schema.String,
26
+ path: Schema.String,
27
+ },
28
+ ) {}
29
+
30
+ export class InvalidProjectName extends Schema.TaggedError<InvalidProjectName>()(
31
+ "InvalidProjectName",
32
+ {
33
+ message: Schema.String,
34
+ path: Schema.String,
35
+ },
36
+ ) {}
37
+
38
+ export class GitInitializationFailed extends Schema.TaggedError<GitInitializationFailed>()(
39
+ "GitInitializationFailed",
40
+ {
41
+ cause: Schema.optional(Schema.Defect()),
42
+ message: Schema.String,
43
+ path: Schema.String,
44
+ },
45
+ ) {}
46
+
47
+ const packageJson = (projectName: string): string =>
48
+ `${JSON.stringify(
49
+ {
50
+ name: projectName,
51
+ private: true,
52
+ version: "0.0.0",
53
+ type: "module",
54
+ scripts: {
55
+ typecheck: "ignotum codegen && tsc --noEmit",
56
+ },
57
+ dependencies: {
58
+ ignotum: "latest",
59
+ },
60
+ devDependencies: {
61
+ typescript: "^7.0.2",
62
+ },
63
+ engines: {
64
+ node: ">=22.18.0",
65
+ },
66
+ },
67
+ null,
68
+ 2,
69
+ )}\n`;
70
+
71
+ const tsconfig = `{
72
+ "compilerOptions": {
73
+ "target": "ES2023",
74
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
75
+ "module": "NodeNext",
76
+ "moduleResolution": "NodeNext",
77
+ "jsx": "react-jsx",
78
+ "jsxImportSource": "ignotum/client",
79
+ "strict": true,
80
+ "noEmit": true,
81
+ "skipLibCheck": true,
82
+ "paths": {
83
+ "@/*": ["./*"]
84
+ }
85
+ },
86
+ "include": ["_generated", "client", "server", "shared"]
87
+ }
88
+ `;
89
+
90
+ const gitignore = `node_modules/
91
+ .ignotum/
92
+
93
+ .env
94
+ .env.*
95
+ !.env.example
96
+
97
+ .DS_Store
98
+ Thumbs.db
99
+ `;
100
+
101
+ const schema = `import { defineSchema } from "ignotum/server";
102
+
103
+ export default defineSchema(({ table, values }) => ({
104
+ counters: table({
105
+ value: values.number(),
106
+ }),
107
+ }));
108
+ `;
109
+
110
+ const counterFunctions = `import { mutation, query, values } from "@/_generated/server.js";
111
+ import { counterIncrement } from "@/shared/utils.js";
112
+
113
+ export const get = query({
114
+ returns: values.number(),
115
+
116
+ handler: function* (ctx) {
117
+ const counters = yield* ctx.db.query("counters").collect();
118
+ return counters[0]?.value ?? 0;
119
+ },
120
+ });
121
+
122
+ export const increment = mutation({
123
+ returns: values.number(),
124
+
125
+ handler: function* (ctx) {
126
+ const counters = yield* ctx.db.query("counters").collect();
127
+ const counter = counters[0];
128
+ const value = (counter?.value ?? 0) + counterIncrement;
129
+
130
+ if (counter === undefined) {
131
+ yield* ctx.db.insert("counters", { value });
132
+ } else {
133
+ yield* ctx.db.patch("counters", counter.id, { value });
134
+ }
135
+
136
+ return value;
137
+ },
138
+ });
139
+ `;
140
+
141
+ const app = `import { Result, useMutation, useQuery } from "ignotum/client";
142
+
143
+ import { api } from "@/_generated/api.js";
144
+ import { counterIncrement } from "@/shared/utils.js";
145
+
146
+ export default function App() {
147
+ const count = useQuery(api.counter.get);
148
+ const increment = useMutation(api.counter.increment);
149
+
150
+ return (
151
+ <main class="mx-auto max-w-sm px-6 py-20 text-center">
152
+ <h1 class="text-2xl font-semibold">Counter</h1>
153
+ {Result.match(count, {
154
+ pending: () => <p class="mt-6">Loading...</p>,
155
+ value: (value) => (
156
+ <>
157
+ <p class="my-6 text-5xl tabular-nums">{value}</p>
158
+ <button
159
+ class="rounded bg-zinc-900 px-4 py-2 text-white"
160
+ type="button"
161
+ onClick={() => void increment()}
162
+ >
163
+ Increment by {counterIncrement}
164
+ </button>
165
+ </>
166
+ ),
167
+ })}
168
+ </main>
169
+ );
170
+ }
171
+ `;
172
+
173
+ const utils = `export const counterIncrement = 1;
174
+ `;
175
+
176
+ const styles = `@import "tailwindcss";
177
+ `;
178
+
179
+ const readme = (projectName: string): string => `# ${projectName}
180
+
181
+ A small Ignotum counter app.
182
+
183
+ ## Getting started
184
+
185
+ You need Node.js 22.18 or newer. The project generator installs dependencies by default. If you
186
+ created the project with \`--no-install\`, install them now:
187
+
188
+ \`\`\`sh
189
+ npx ignotum install
190
+ \`\`\`
191
+
192
+ This command uses pnpm when it is installed and otherwise uses npm.
193
+
194
+ Start the app:
195
+
196
+ \`\`\`sh
197
+ npx ignotum dev
198
+ \`\`\`
199
+
200
+ Open <http://127.0.0.1:3210>.
201
+
202
+ The project generator creates \`_generated\`. The dev server checks those files before it starts
203
+ and updates them when the schema or server functions change.
204
+
205
+ ## Project files
206
+
207
+ - \`server/schema.ts\` defines the database tables.
208
+ - \`server/counter.ts\` defines the query and mutation used by the counter.
209
+ - \`client/App.tsx\` is the UI.
210
+ - \`client/styles.css\` loads Tailwind CSS.
211
+ - \`shared/utils.ts\` contains code shared across the app.
212
+ - \`_generated\` contains Ignotum's generated types and bindings. Do not edit it by hand.
213
+
214
+ Run the typechecker after a change:
215
+
216
+ \`\`\`sh
217
+ npx ignotum codegen
218
+ npx tsc --noEmit
219
+ \`\`\`
220
+
221
+ ## Claude Code
222
+
223
+ If you use Claude Code, rename \`AGENTS.md\` to \`CLAUDE.md\` and \`.agents\` to \`.claude\` so it
224
+ can find the project instructions and Ignotum skill.
225
+ `;
226
+
227
+ const projectFiles = (projectName: string): ReadonlyArray<ProjectFile> => [
228
+ { content: app, path: "client/App.tsx" },
229
+ { content: styles, path: "client/styles.css" },
230
+ { content: counterFunctions, path: "server/counter.ts" },
231
+ { content: schema, path: "server/schema.ts" },
232
+ { content: utils, path: "shared/utils.ts" },
233
+ { content: packageJson(projectName), path: "package.json" },
234
+ { content: tsconfig, path: "tsconfig.json" },
235
+ { content: gitignore, path: ".gitignore" },
236
+ { content: readme(projectName), path: "README.md" },
237
+ ...agentProjectFiles,
238
+ ];
239
+
240
+ const writeProjectFile = Effect.fn("NewProject.writeProjectFile")(function* (
241
+ projectDirectory: string,
242
+ file: ProjectFile,
243
+ ) {
244
+ const fileSystem = yield* FileSystem.FileSystem;
245
+ const path = yield* Path.Path;
246
+ const filePath = path.join(projectDirectory, file.path);
247
+
248
+ yield* fileSystem.makeDirectory(path.dirname(filePath), { recursive: true });
249
+ yield* fileSystem.writeFileString(filePath, file.content, { flag: "wx" });
250
+ });
251
+
252
+ const gitExitCode = Effect.fn("NewProject.gitExitCode")(function* (
253
+ projectDirectory: string,
254
+ args: ReadonlyArray<string>,
255
+ ) {
256
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
257
+ return yield* spawner
258
+ .exitCode(
259
+ ChildProcess.make("git", args, {
260
+ cwd: projectDirectory,
261
+ stderr: "ignore",
262
+ stdout: "ignore",
263
+ }),
264
+ )
265
+ .pipe(
266
+ Effect.mapError((cause) =>
267
+ GitInitializationFailed.make({
268
+ cause,
269
+ message: `Could not run Git in ${projectDirectory}.`,
270
+ path: projectDirectory,
271
+ }),
272
+ ),
273
+ );
274
+ });
275
+
276
+ const runGit = Effect.fn("NewProject.runGit")(function* (
277
+ projectDirectory: string,
278
+ args: ReadonlyArray<string>,
279
+ action: string,
280
+ ) {
281
+ const exitCode = yield* gitExitCode(projectDirectory, args);
282
+
283
+ if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
284
+ return yield* GitInitializationFailed.make({
285
+ message: `Git exited with code ${exitCode} while ${action} in ${projectDirectory}.`,
286
+ path: projectDirectory,
287
+ });
288
+ }
289
+ });
290
+
291
+ const initializeGit = Effect.fn("NewProject.initializeGit")(function* (projectDirectory: string) {
292
+ yield* runGit(projectDirectory, ["init", "--quiet"], "initializing the repository");
293
+ yield* runGit(projectDirectory, ["add", "--all"], "staging the initial files");
294
+
295
+ const [hasName, hasEmail] = yield* Effect.all(
296
+ [
297
+ gitExitCode(projectDirectory, ["config", "user.name"]),
298
+ gitExitCode(projectDirectory, ["config", "user.email"]),
299
+ ],
300
+ { concurrency: "unbounded" },
301
+ ).pipe(
302
+ Effect.map((exitCodes) =>
303
+ exitCodes.map((exitCode) => exitCode === ChildProcessSpawner.ExitCode(0)),
304
+ ),
305
+ );
306
+ const identity =
307
+ hasName === true && hasEmail === true
308
+ ? []
309
+ : ["-c", "user.name=Ignotum", "-c", "user.email=ignotum@localhost"];
310
+
311
+ yield* runGit(
312
+ projectDirectory,
313
+ [...identity, "commit", "--quiet", "--no-gpg-sign", "-m", "Init"],
314
+ "creating the initial commit",
315
+ );
316
+ });
317
+
318
+ export const createProject = Effect.fn("NewProject.createProject")(function* (
319
+ options: NewProjectOptions,
320
+ ) {
321
+ const fileSystem = yield* FileSystem.FileSystem;
322
+ const path = yield* Path.Path;
323
+ const projectDirectory = path.resolve(options.currentDirectory, options.directory);
324
+ const projectName = String.kebabCase(path.basename(projectDirectory));
325
+
326
+ if (projectName.length === 0) {
327
+ return yield* InvalidProjectName.make({
328
+ message: `Could not derive a package name from ${projectDirectory}.`,
329
+ path: projectDirectory,
330
+ });
331
+ }
332
+
333
+ const exists = yield* fileSystem.exists(projectDirectory);
334
+
335
+ if (exists) {
336
+ const info = yield* fileSystem.stat(projectDirectory);
337
+ if (info.type !== "Directory") {
338
+ return yield* ProjectDirectoryNotEmpty.make({
339
+ message: `${projectDirectory} already exists and is not an empty directory.`,
340
+ path: projectDirectory,
341
+ });
342
+ }
343
+
344
+ const entries = yield* fileSystem.readDirectory(projectDirectory);
345
+ if (entries.length > 0) {
346
+ return yield* ProjectDirectoryNotEmpty.make({
347
+ message: `${projectDirectory} is not empty. Choose an empty directory.`,
348
+ path: projectDirectory,
349
+ });
350
+ }
351
+ } else {
352
+ yield* fileSystem.makeDirectory(projectDirectory, { recursive: true });
353
+ }
354
+
355
+ yield* Effect.forEach(
356
+ projectFiles(projectName),
357
+ (file) => writeProjectFile(projectDirectory, file),
358
+ {
359
+ discard: true,
360
+ },
361
+ );
362
+
363
+ yield* generate(projectDirectory);
364
+
365
+ const packageManager = options.install ? yield* installDependencies(projectDirectory) : null;
366
+
367
+ if (options.git) {
368
+ yield* initializeGit(projectDirectory);
369
+ }
370
+
371
+ return {
372
+ directory: projectDirectory,
373
+ gitInitialized: options.git,
374
+ packageManager,
375
+ projectName,
376
+ } satisfies NewProjectResult;
377
+ });
@@ -0,0 +1,55 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
3
+
4
+ export type PackageManager = "npm" | "pnpm";
5
+
6
+ export class DependencyInstallationFailed extends Schema.TaggedError<DependencyInstallationFailed>()(
7
+ "DependencyInstallationFailed",
8
+ {
9
+ cause: Schema.optional(Schema.Defect()),
10
+ message: Schema.String,
11
+ path: Schema.String,
12
+ },
13
+ ) {}
14
+
15
+ const installWith = Effect.fn("PackageManager.installWith")(function* (
16
+ projectDirectory: string,
17
+ packageManager: PackageManager,
18
+ ) {
19
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
20
+ const exitCode = yield* spawner.exitCode(
21
+ ChildProcess.make(packageManager, ["install"], {
22
+ cwd: projectDirectory,
23
+ stderr: "inherit",
24
+ stdin: "inherit",
25
+ stdout: "inherit",
26
+ }),
27
+ );
28
+
29
+ if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
30
+ return yield* DependencyInstallationFailed.make({
31
+ message: `${packageManager} install exited with code ${exitCode} in ${projectDirectory}.`,
32
+ path: projectDirectory,
33
+ });
34
+ }
35
+
36
+ return packageManager;
37
+ });
38
+
39
+ export const installDependencies = Effect.fn("PackageManager.installDependencies")(function* (
40
+ projectDirectory: string,
41
+ ) {
42
+ return yield* installWith(projectDirectory, "pnpm").pipe(
43
+ Effect.catchReasons("PlatformError", {
44
+ NotFound: () => installWith(projectDirectory, "npm"),
45
+ }),
46
+ Effect.catchTags({
47
+ PlatformError: (cause) =>
48
+ DependencyInstallationFailed.make({
49
+ cause,
50
+ message: `Could not run pnpm or npm in ${projectDirectory}.`,
51
+ path: projectDirectory,
52
+ }),
53
+ }),
54
+ );
55
+ });
@@ -0,0 +1,83 @@
1
+ /** @effect-diagnostics globalConsole:skip-file */
2
+ import { Cause, Effect, ErrorReporter, Schema } from "effect";
3
+
4
+ import {
5
+ FunctionAddress,
6
+ Operation,
7
+ ProtocolErrorCode,
8
+ RequestId,
9
+ } from "@ignotum/contracts/runtime/sync";
10
+
11
+ export class ConnectionUnavailable extends Schema.TaggedError<ConnectionUnavailable>()(
12
+ "ConnectionUnavailable",
13
+ {
14
+ cause: Schema.optional(Schema.Defect()),
15
+ message: Schema.String,
16
+ },
17
+ ) {}
18
+
19
+ export class InvalidClientMessage extends Schema.TaggedError<InvalidClientMessage>()(
20
+ "InvalidClientMessage",
21
+ {
22
+ cause: Schema.Defect(),
23
+ message: Schema.String,
24
+ },
25
+ ) {}
26
+
27
+ export class InvalidMutationArguments extends Schema.TaggedError<InvalidMutationArguments>()(
28
+ "InvalidMutationArguments",
29
+ {
30
+ cause: Schema.Defect(),
31
+ function: FunctionAddress,
32
+ message: Schema.String,
33
+ },
34
+ ) {}
35
+
36
+ export class InvalidServerMessage extends Schema.TaggedError<InvalidServerMessage>()(
37
+ "InvalidServerMessage",
38
+ {
39
+ cause: Schema.Defect(),
40
+ message: Schema.String,
41
+ },
42
+ ) {}
43
+
44
+ export class ServerProtocolError extends Schema.TaggedError<ServerProtocolError>()(
45
+ "ServerProtocolError",
46
+ {
47
+ code: ProtocolErrorCode,
48
+ message: Schema.String,
49
+ operation: Schema.optional(Operation),
50
+ },
51
+ ) {}
52
+
53
+ export class MutationOutcomeUnknown extends Schema.TaggedError<MutationOutcomeUnknown>()(
54
+ "MutationOutcomeUnknown",
55
+ {
56
+ cause: Schema.optional(Schema.Defect()),
57
+ function: FunctionAddress,
58
+ id: RequestId,
59
+ message: Schema.String,
60
+ },
61
+ ) {}
62
+
63
+ export const ClientInfrastructureError = Schema.Union([
64
+ ConnectionUnavailable,
65
+ InvalidClientMessage,
66
+ InvalidMutationArguments,
67
+ InvalidServerMessage,
68
+ ServerProtocolError,
69
+ MutationOutcomeUnknown,
70
+ ]);
71
+ export type ClientInfrastructureError = typeof ClientInfrastructureError.Type;
72
+
73
+ const browserErrorReporter = ErrorReporter.make(({ attributes, error }) => {
74
+ globalThis.console.error("Ignotum infrastructure error", error, attributes);
75
+ });
76
+
77
+ export const clientErrorReporterLayer = ErrorReporter.layer([browserErrorReporter]);
78
+
79
+ export const reportClientError = Effect.fn("ClientErrorReporter.report")(function* (
80
+ error: ClientInfrastructureError,
81
+ ) {
82
+ yield* ErrorReporter.report(Cause.fail(error));
83
+ });
@@ -0,0 +1,141 @@
1
+ import { Effect, Schema } from "effect";
2
+ import { useDebugValue, useEffect, useMemo, useState } from "preact/hooks";
3
+
4
+ import { functionPathOf, type FunctionReference } from "../internal/api.js";
5
+ import { pending } from "@ignotum/contracts/runtime/result";
6
+ import type { ErrorValue, QueryResult, SettledResult } from "@ignotum/contracts/runtime/result";
7
+ import { encodeTransportObject } from "@ignotum/contracts/runtime/sync";
8
+ import {
9
+ ClientInfrastructureError,
10
+ InvalidMutationArguments,
11
+ reportClientError,
12
+ } from "./errors.js";
13
+ import { isQuerySkip, type QuerySkip } from "./query.js";
14
+ import { SyncClient, syncClientInternals, syncRuntime } from "./sync.js";
15
+
16
+ // @effect-diagnostics-next-line missingPipeableSignature:off React hooks are not pipeable functions.
17
+ export function useQuery<Success, Failure extends ErrorValue>(
18
+ reference: FunctionReference<"Query", void, Success, Failure>,
19
+ ): QueryResult<Success, Failure>;
20
+ export function useQuery<Success, Failure extends ErrorValue>(
21
+ reference: FunctionReference<"Query", void, Success, Failure>,
22
+ args: QuerySkip,
23
+ ): QueryResult<Success, Failure>;
24
+ export function useQuery<Args extends object, Success, Failure extends ErrorValue>(
25
+ reference: FunctionReference<"Query", Args, Success, Failure>,
26
+ args: NoInfer<Args> | QuerySkip,
27
+ ): QueryResult<Success, Failure>;
28
+ // @effect-diagnostics-next-line missingPipeableSignature:off React hooks must remain direct calls so hook order is statically visible.
29
+ export function useQuery<Args extends object, Success, Failure extends ErrorValue>(
30
+ reference: FunctionReference<"Query", Args | void, Success, Failure>,
31
+ args?: Args | QuerySkip,
32
+ ): QueryResult<Success, Failure> {
33
+ const functionPath = functionPathOf(reference);
34
+ const skipped = isQuerySkip(args);
35
+ const input = args === undefined || skipped ? {} : args;
36
+ const jsonArgs = encodeTransportObject(input);
37
+ const identity = skipped
38
+ ? `skip:${functionPath}`
39
+ : syncClientInternals.queryIdentity(functionPath, jsonArgs);
40
+ const [state, setState] = useState<{
41
+ readonly identity: string;
42
+ readonly result: QueryResult<Success, Failure> | ClientInfrastructureError;
43
+ }>(() => ({ identity, result: pending() }));
44
+
45
+ useDebugValue({ args: input, function: functionPath, result: state.result });
46
+ useEffect(() => {
47
+ if (skipped) return;
48
+
49
+ let active = true;
50
+ let release: (() => void) | undefined;
51
+
52
+ void syncRuntime
53
+ .runPromise(
54
+ Effect.gen(function* () {
55
+ const client = yield* SyncClient;
56
+ return yield* client.observe(functionPath, jsonArgs);
57
+ }),
58
+ )
59
+ .then(
60
+ (observer) => {
61
+ if (!active) {
62
+ syncRuntime.runFork(observer.release);
63
+ return;
64
+ }
65
+ const update = () => {
66
+ // SAFETY: the function reference couples this subscription's runtime
67
+ // path to its generated success and failure types.
68
+ const result = observer.getSnapshot() as
69
+ | QueryResult<Success, Failure>
70
+ | ClientInfrastructureError;
71
+ setState({ identity, result });
72
+ };
73
+ release = observer.subscribe(update);
74
+ update();
75
+ const releaseObserver = release;
76
+ release = () => {
77
+ releaseObserver();
78
+ syncRuntime.runFork(observer.release);
79
+ };
80
+ },
81
+ (error) => {
82
+ if (active && Schema.is(ClientInfrastructureError)(error)) {
83
+ setState({ identity, result: error });
84
+ }
85
+ },
86
+ );
87
+
88
+ return () => {
89
+ active = false;
90
+ release?.();
91
+ };
92
+ }, [identity]);
93
+
94
+ if (skipped) return pending();
95
+
96
+ if (state.identity !== identity) {
97
+ return pending();
98
+ }
99
+
100
+ const result = state.result;
101
+ if (Schema.is(ClientInfrastructureError)(result)) throw result;
102
+ return result;
103
+ }
104
+
105
+ type Mutation<Args, Success, Failure extends ErrorValue> = [Args] extends [void]
106
+ ? () => Promise<SettledResult<Success, Failure>>
107
+ : (args: Args) => Promise<SettledResult<Success, Failure>>;
108
+
109
+ export const useMutation = <Args extends object | void, Success, Failure extends ErrorValue>(
110
+ reference: FunctionReference<"Mutation", Args, Success, Failure>,
111
+ ): Mutation<Args, Success, Failure> =>
112
+ useMemo(() => {
113
+ const mutate = (args: Args | undefined) => {
114
+ const functionPath = functionPathOf(reference);
115
+ const input = args === undefined ? {} : args;
116
+ return syncRuntime.runPromise<SettledResult<Success, Failure>, ClientInfrastructureError>(
117
+ Effect.gen(function* () {
118
+ const client = yield* SyncClient;
119
+ const jsonArgs = yield* Effect.try({
120
+ try: () => encodeTransportObject(input),
121
+ catch: (cause) =>
122
+ InvalidMutationArguments.make({
123
+ cause,
124
+ function: functionPath,
125
+ message: `Mutation arguments for ${functionPath} contain an unsupported value.`,
126
+ }),
127
+ }).pipe(Effect.tapError(reportClientError));
128
+ // SAFETY: generated function references bind the runtime path to the
129
+ // declared public result types validated by the server executor.
130
+ // oxlint-disable-next-line anti-slop/no-chained-type-assertions -- The untyped sync transport deliberately erases the generated reference's result parameters.
131
+ return (yield* client.mutate(functionPath, jsonArgs)) as unknown as SettledResult<
132
+ Success,
133
+ Failure
134
+ >;
135
+ }),
136
+ );
137
+ };
138
+ // SAFETY: the builder gives argument-free functions Args = void. At runtime
139
+ // both call shapes normalize omitted arguments to the empty JSON object.
140
+ return mutate as Mutation<Args, Success, Failure>;
141
+ }, [reference]);